Skip to content

XSS Protection: Preventing Cross-Site Scripting in Backend APIs

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about XSS Protection: Preventing Cross. We cover key concepts, practical examples, and best practices to help you master this topic.

Cross-Site Scripting (XSS) is a vulnerability where attackers inject malicious scripts into web pages viewed by other users. In backend APIs, XSS typically occurs when user-generated content is stored and later served without proper encoding, allowing scripts to execute in victims' browsers.

flowchart TB
    Attacker -->|Submit: | API
    API -->|Store Unsanitized| DB[(Database)]
    Victim -->|Request Page| Server
    Server -->|Retrieve Stored Content| DB
    Server -->|Serve Without Encoding| Victim
    Victim -->|Execute Script| Cookie[Cookie Stolen]

    Attacker -.->|Submit Same| SafeAPI[API with Sanitization]
    SafeAPI -->|Sanitize & Encode| SafeDB[(Database)]
    Victim -.->|Request Page| SafeServer
    SafeServer -->|Encode Output: <script>| SafeVictim
    SafeVictim -->|Display as Text| Safe[No Script Execution]

What You'll Learn

  • Types of XSS: stored, reflected, and DOM-based
  • Output encoding and sanitization with DOMPurify and similar libraries
  • Content Security Policy (CSP) headers for defense in depth
  • API Design Patterns that reduce XSS risk

Why It Matters

XSS can lead to Session Hijacking, credential theft, defacement, and malware distribution. Even backend APIs that serve JSON are vulnerable if the consuming client renders HTML without proper encoding.

Real-World Use

A social media platform suffered a stored XSS attack when users posted comments containing <script> tags. The backend stored comments as-is and served them in API responses. The frontend rendered comments using innerHTML without sanitization, executing the attacker's script in every viewer's browser.

XSS Prevention Techniques

Output Encoding for API Responses

const he = require('he');

function encodeForHTML(input) {
  return he.encode(input, { useNamedReferences: true });
}

app.post('/api/comments', async (req, res) => {
  const comment = {
    text: req.body.text,
    // Store original but encode for display
    textEncoded: encodeForHTML(req.body.text),
    userId: req.session.userId,
    createdAt: new Date()
  };
  await db.insertComment(comment);
  res.status(201).json(comment);
});

app.get('/api/comments', async (req, res) => {
  const comments = await db.getComments();
  // Return encoded text for safe rendering
  res.json(comments.map(c => ({
    ...c,
    text: c.textEncoded
  })));
});

Expected output:

Input: <script>alert('xss')</script>
Stored as: &lt;script&gt;alert('xss')&lt;/script&gt;
Rendered as text, not executed.

Content Security Policy Headers

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'nonce-random123'"],
    styleSrc: ["'self'", "'nonce-random123'"],
    imgSrc: ["'self'", 'https://images.example.com'],
    connectSrc: ["'self'"],
    fontSrc: ["'self'"],
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    baseUri: ["'self'"]
  }
}));

// For inline scripts, use nonce
res.set('Content-Security-Policy', "script-src 'nonce-r4nd0m'");

Expected output:

Browser blocks inline scripts without the correct nonce. Blocks scripts from external domains. Blocks eval(). Blocks object/embed tags.

Sanitization with DOMPurify on Server

const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

function sanitizeHTML(dirty) {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'title'],
    ALLOW_DATA_ATTR: false
  });
}

app.post('/api/profile/bio', async (req, res) => {
  const clean = sanitizeHTML(req.body.bio);
  await db.updateUserBio(req.session.userId, clean);
  res.json({ bio: clean });
});

Expected output:

Input: <script>alert('xss')</script><b>Hello</b>
Output: <b>Hello</b> (script tag removed, bold tag preserved)

Common Mistakes

  • Relying only on frontend sanitization — attackers can call APIs directly. Always sanitize on the backend.
  • Using blacklist filtering (blocking <script> tags) instead of whitelist (allowing only safe tags).
  • Setting CSP headers too permissively (e.g., script-src 'unsafe-inline'), negating the protection.
  • Forgetting to encode data in JSON responses if the consumer renders it as HTML.
  • Not encoding URLs or CSS values — XSS can occur through <a href="/programming-languages/javascript/">JavaScript</a>: URLs in links.

Practice Questions

  1. What is the difference between stored and reflected XSS?
  2. How does output encoding prevent XSS?
  3. What is Content Security Policy and how does it help?
  4. Why is whitelist-based sanitization better than blacklist?
  5. How can an API that returns JSON still be vulnerable to XSS?

Challenge

Build a comment system API where users can post comments with basic HTML formatting (bold, italic, links). Implement backend sanitization using DOMPurify that strips all script tags and event handlers but allows safe HTML. Add CSP headers. Test with XSS payloads.

FAQ

What is XSS (Cross-Site Scripting)?

XSS is a security vulnerability where attackers inject malicious scripts into web pages. These scripts execute in victims' browsers, allowing cookie theft, session hijacking, and content manipulation.

What is the difference between stored and reflected XSS?

Stored XSS: malicious script is stored in the database and served to all users who view the affected page. Reflected XSS: malicious script is in the URL and reflected back immediately.

How does Content Security Policy prevent XSS?

CSP is an HTTP header that tells the browser which sources of content are trusted. It blocks inline scripts, eval(), and scripts from untrusted domains even if injected.

Do I need to sanitize input if I use parameterized queries?

Yes. Parameterized queries prevent SQL injection but do not prevent XSS. XSS is about output encoding and sanitization, independent of database queries.

Should I sanitize on the frontend or backend?

Both. Frontend sanitization improves UX by providing immediate feedback. Backend sanitization is essential because APIs can be called directly.

Mini Project

Build a forum API with posts and comments. Implement: (1) server-side HTML sanitization with DOMPurify, (2) CSP headers via Helmet, (3) output encoding for all user-generated content in API responses, (4) a test suite that attempts 10 different XSS payloads and verifies they are neutralized.

What's Next

Continue to CSRF Protection to prevent cross-site request forgery attacks.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro