xss-prevention

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

XSS Prevention

XSS防护

Overview

概述

Implement comprehensive Cross-Site Scripting (XSS) prevention using input sanitization, output encoding, CSP headers, and secure coding practices.
使用输入清理、输出编码、CSP标头和安全编码实践,实现全面的跨站脚本(XSS)防护。

When to Use

适用场景

  • User-generated content display
  • Rich text editors
  • Comment systems
  • Search functionality
  • Dynamic HTML generation
  • Template rendering
  • 用户生成内容展示
  • 富文本编辑器
  • 评论系统
  • 搜索功能
  • 动态HTML生成
  • 模板渲染

Implementation Examples

实现示例

1. Node.js XSS Prevention

1. Node.js XSS防护

javascript
// xss-prevention.js
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const he = require('he');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

class XSSPrevention {
  /**
   * HTML Entity Encoding - Safest for text content
   */
  static encodeHTML(str) {
    return he.encode(str, {
      useNamedReferences: true,
      encodeEverything: false
    });
  }

  /**
   * Sanitize HTML - For rich content
   */
  static sanitizeHTML(dirty) {
    const config = {
      ALLOWED_TAGS: [
        'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3',
        'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code'
      ],
      ALLOWED_ATTR: [
        'href', 'src', 'alt', 'title', 'class'
      ],
      ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i,
      KEEP_CONTENT: true,
      RETURN_DOM: false,
      RETURN_DOM_FRAGMENT: false
    };

    return DOMPurify.sanitize(dirty, config);
  }

  /**
   * Strict sanitization - For untrusted HTML
   */
  static sanitizeStrict(dirty) {
    return DOMPurify.sanitize(dirty, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
      ALLOWED_ATTR: [],
      KEEP_CONTENT: true
    });
  }

  /**
   * JavaScript context encoding
   */
  static encodeForJS(str) {
    return str.replace(/[<>"'&]/g, (char) => {
      const escape = {
        '<': '\\x3C',
        '>': '\\x3E',
        '"': '\\x22',
        "'": '\\x27',
        '&': '\\x26'
      };
      return escape[char];
    });
  }

  /**
   * URL parameter encoding
   */
  static encodeURL(str) {
    return encodeURIComponent(str);
  }

  /**
   * Attribute context encoding
   */
  static encodeAttribute(str) {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;')
      .replace(/\//g, '&#x2F;');
  }

  /**
   * Validate and sanitize URLs
   */
  static sanitizeURL(url) {
    try {
      const parsed = new URL(url);

      // Only allow safe protocols
      if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
        return '';
      }

      return parsed.href;
    } catch {
      return '';
    }
  }

  /**
   * Strip all HTML tags
   */
  static stripHTML(str) {
    return str.replace(/<[^>]*>/g, '');
  }

  /**
   * React-style JSX escaping
   */
  static escapeForReact(str) {
    return {
      __html: DOMPurify.sanitize(str)
    };
  }
}

// Express middleware
function xssProtection(req, res, next) {
  // Sanitize request body
  if (req.body) {
    req.body = sanitizeObject(req.body);
  }

  // Sanitize query parameters
  if (req.query) {
    req.query = sanitizeObject(req.query);
  }

  next();
}

function sanitizeObject(obj) {
  const sanitized = {};

  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'string') {
      sanitized[key] = XSSPrevention.stripHTML(value);
    } else if (typeof value === 'object' && value !== null) {
      sanitized[key] = sanitizeObject(value);
    } else {
      sanitized[key] = value;
    }
  }

  return sanitized;
}

// Express example
const express = require('express');
const app = express();

app.use(express.json());
app.use(xssProtection);

app.post('/api/comments', (req, res) => {
  const { comment } = req.body;

  // Additional sanitization for rich content
  const safeComment = XSSPrevention.sanitizeHTML(comment);

  // Store in database
  // db.comments.insert({ content: safeComment });

  res.json({ comment: safeComment });
});

module.exports = XSSPrevention;
javascript
// xss-prevention.js
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const he = require('he');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

class XSSPrevention {
  /**
   * HTML Entity Encoding - Safest for text content
   */
  static encodeHTML(str) {
    return he.encode(str, {
      useNamedReferences: true,
      encodeEverything: false
    });
  }

  /**
   * Sanitize HTML - For rich content
   */
  static sanitizeHTML(dirty) {
    const config = {
      ALLOWED_TAGS: [
        'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3',
        'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code'
      ],
      ALLOWED_ATTR: [
        'href', 'src', 'alt', 'title', 'class'
      ],
      ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i,
      KEEP_CONTENT: true,
      RETURN_DOM: false,
      RETURN_DOM_FRAGMENT: false
    };

    return DOMPurify.sanitize(dirty, config);
  }

  /**
   * Strict sanitization - For untrusted HTML
   */
  static sanitizeStrict(dirty) {
    return DOMPurify.sanitize(dirty, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
      ALLOWED_ATTR: [],
      KEEP_CONTENT: true
    });
  }

  /**
   * JavaScript context encoding
   */
  static encodeForJS(str) {
    return str.replace(/[<>"'&]/g, (char) => {
      const escape = {
        '<': '\\x3C',
        '>': '\\x3E',
        "'": '\\x22',
        "'": '\\x27',
        '&': '\\x26'
      };
      return escape[char];
    });
  }

  /**
   * URL parameter encoding
   */
  static encodeURL(str) {
    return encodeURIComponent(str);
  }

  /**
   * Attribute context encoding
   */
  static encodeAttribute(str) {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;')
      .replace(/\//g, '&#x2F;');
  }

  /**
   * Validate and sanitize URLs
   */
  static sanitizeURL(url) {
    try {
      const parsed = new URL(url);

      // Only allow safe protocols
      if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
        return '';
      }

      return parsed.href;
    } catch {
      return '';
    }
  }

  /**
   * Strip all HTML tags
   */
  static stripHTML(str) {
    return str.replace(/<[^>]*>/g, '');
  }

  /**
   * React-style JSX escaping
   */
  static escapeForReact(str) {
    return {
      __html: DOMPurify.sanitize(str)
    };
  }
}

// Express middleware
function xssProtection(req, res, next) {
  // Sanitize request body
  if (req.body) {
    req.body = sanitizeObject(req.body);
  }

  // Sanitize query parameters
  if (req.query) {
    req.query = sanitizeObject(req.query);
  }

  next();
}

function sanitizeObject(obj) {
  const sanitized = {};

  for (const [key, value] of Object.entries(obj)) {
    if (typeof value === 'string') {
      sanitized[key] = XSSPrevention.stripHTML(value);
    } else if (typeof value === 'object' && value !== null) {
      sanitized[key] = sanitizeObject(value);
    } else {
      sanitized[key] = value;
    }
  }

  return sanitized;
}

// Express example
const express = require('express');
const app = express();

app.use(express.json());
app.use(xssProtection);

app.post('/api/comments', (req, res) => {
  const { comment } = req.body;

  // Additional sanitization for rich content
  const safeComment = XSSPrevention.sanitizeHTML(comment);

  // Store in database
  // db.comments.insert({ content: safeComment });

  res.json({ comment: safeComment });
});

module.exports = XSSPrevention;

2. Python XSS Prevention

2. Python XSS防护

python
undefined
python
undefined

xss_prevention.py

xss_prevention.py

import html import bleach from urllib.parse import urlparse, quote import re
class XSSPrevention: # Allowed HTML tags for rich content ALLOWED_TAGS = [ 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'blockquote', 'code' ]
ALLOWED_ATTRIBUTES = {
    'a': ['href', 'title'],
    'img': ['src', 'alt']
}

@staticmethod
def encode_html(text: str) -> str:
    """HTML entity encoding - safest for text content"""
    return html.escape(text, quote=True)

@staticmethod
def sanitize_html(dirty_html: str) -> str:
    """Sanitize HTML - for rich content"""
    return bleach.clean(
        dirty_html,
        tags=XSSPrevention.ALLOWED_TAGS,
        attributes=XSSPrevention.ALLOWED_ATTRIBUTES,
        strip=True
    )

@staticmethod
def sanitize_strict(dirty_html: str) -> str:
    """Strict sanitization - strip all HTML"""
    return bleach.clean(
        dirty_html,
        tags=[],
        attributes={},
        strip=True
    )

@staticmethod
def strip_html(text: str) -> str:
    """Remove all HTML tags"""
    return re.sub(r'<[^>]*>', '', text)

@staticmethod
def sanitize_url(url: str) -> str:
    """Validate and sanitize URLs"""
    try:
        parsed = urlparse(url)

        # Only allow safe protocols
        if parsed.scheme not in ['http', 'https', 'mailto']:
            return ''

        return url
    except:
        return ''

@staticmethod
def encode_for_javascript(text: str) -> str:
    """Encode for JavaScript context"""
    escape_map = {
        '<': '\\x3C',
        '>': '\\x3E',
        '"': '\\x22',
        "'": '\\x27',
        '&': '\\x26',
        '/': '\\x2F'
    }

    return ''.join(escape_map.get(char, char) for char in text)

@staticmethod
def encode_url_param(text: str) -> str:
    """Encode for URL parameters"""
    return quote(text, safe='')
import html import bleach from urllib.parse import urlparse, quote import re
class XSSPrevention: # Allowed HTML tags for rich content ALLOWED_TAGS = [ 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'blockquote', 'code' ]
ALLOWED_ATTRIBUTES = {
    'a': ['href', 'title'],
    'img': ['src', 'alt']
}

@staticmethod
def encode_html(text: str) -> str:
    """HTML entity encoding - safest for text content"""
    return html.escape(text, quote=True)

@staticmethod
def sanitize_html(dirty_html: str) -> str:
    """Sanitize HTML - for rich content"""
    return bleach.clean(
        dirty_html,
        tags=XSSPrevention.ALLOWED_TAGS,
        attributes=XSSPrevention.ALLOWED_ATTRIBUTES,
        strip=True
    )

@staticmethod
def sanitize_strict(dirty_html: str) -> str:
    """Strict sanitization - strip all HTML"""
    return bleach.clean(
        dirty_html,
        tags=[],
        attributes={},
        strip=True
    )

@staticmethod
def strip_html(text: str) -> str:
    """Remove all HTML tags"""
    return re.sub(r'<[^>]*>', '', text)

@staticmethod
def sanitize_url(url: str) -> str:
    """Validate and sanitize URLs"""
    try:
        parsed = urlparse(url)

        # Only allow safe protocols
        if parsed.scheme not in ['http', 'https', 'mailto']:
            return ''

        return url
    except:
        return ''

@staticmethod
def encode_for_javascript(text: str) -> str:
    """Encode for JavaScript context"""
    escape_map = {
        '<': '\\x3C',
        '>': '\\x3E',
        "'": '\\x22',
        "'": '\\x27',
        '&': '\\x26',
        '/': '\\x2F'
    }

    return ''.join(escape_map.get(char, char) for char in text)

@staticmethod
def encode_url_param(text: str) -> str:
    """Encode for URL parameters"""
    return quote(text, safe='')

Flask integration

Flask integration

from flask import Flask, request, jsonify from functools import wraps
app = Flask(name)
def sanitize_input(f): """Decorator to sanitize all request inputs""" @wraps(f) def decorated_function(*args, **kwargs): if request.is_json: data = request.get_json() request._cached_json = sanitize_dict(data)
    return f(*args, **kwargs)

return decorated_function
def sanitize_dict(data: dict) -> dict: """Recursively sanitize dictionary values""" sanitized = {}
for key, value in data.items():
    if isinstance(value, str):
        sanitized[key] = XSSPrevention.strip_html(value)
    elif isinstance(value, dict):
        sanitized[key] = sanitize_dict(value)
    elif isinstance(value, list):
        sanitized[key] = [
            sanitize_dict(item) if isinstance(item, dict)
            else XSSPrevention.strip_html(item) if isinstance(item, str)
            else item
            for item in value
        ]
    else:
        sanitized[key] = value

return sanitized
@app.route('/api/comments', methods=['POST']) @sanitize_input def create_comment(): data = request.get_json() comment = data.get('comment', '')
# Additional rich content sanitization
safe_comment = XSSPrevention.sanitize_html(comment)

return jsonify({'comment': safe_comment})
from flask import Flask, request, jsonify from functools import wraps
app = Flask(name)
def sanitize_input(f): """Decorator to sanitize all request inputs""" @wraps(f) def decorated_function(*args, **kwargs): if request.is_json: data = request.get_json() request._cached_json = sanitize_dict(data)
    return f(*args, **kwargs)

return decorated_function
def sanitize_dict(data: dict) -> dict: """Recursively sanitize dictionary values""" sanitized = {}
for key, value in data.items():
    if isinstance(value, str):
        sanitized[key] = XSSPrevention.strip_html(value)
    elif isinstance(value, dict):
        sanitized[key] = sanitize_dict(value)
    elif isinstance(value, list):
        sanitized[key] = [
            sanitize_dict(item) if isinstance(item, dict)
            else XSSPrevention.strip_html(item) if isinstance(item, str)
            else item
            for item in value
        ]
    else:
        sanitized[key] = value

return sanitized
@app.route('/api/comments', methods=['POST']) @sanitize_input def create_comment(): data = request.get_json() comment = data.get('comment', '')
# Additional rich content sanitization
safe_comment = XSSPrevention.sanitize_html(comment)

return jsonify({'comment': safe_comment})

Django template filter

Django template filter

from django import template from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(name='sanitize_html') def sanitize_html_filter(value): """Django template filter for HTML sanitization""" sanitized = XSSPrevention.sanitize_html(value) return mark_safe(sanitized)
from django import template from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(name='sanitize_html') def sanitize_html_filter(value): """Django template filter for HTML sanitization""" sanitized = XSSPrevention.sanitize_html(value) return mark_safe(sanitized)

Usage in templates:

Usage in templates:

{{ user_content|sanitize_html }}

{{ user_content|sanitize_html }}

undefined
undefined

3. React XSS Prevention

3. React XSS防护

javascript
// XSSSafeComponent.jsx
import React from 'react';
import DOMPurify from 'dompurify';

// Safe text rendering (React automatically escapes)
function SafeText({ text }) {
  return <div>{text}</div>;
}

// Sanitized HTML rendering
function SafeHTML({ html }) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a'],
    ALLOWED_ATTR: ['href']
  });

  return (
    <div
      dangerouslySetInnerHTML={{ __html: sanitized }}
    />
  );
}

// Safe URL attribute
function SafeLink({ href, children }) {
  const safeHref = sanitizeURL(href);

  return (
    <a
      href={safeHref}
      rel="noopener noreferrer"
      target="_blank"
    >
      {children}
    </a>
  );
}

function sanitizeURL(url) {
  try {
    const parsed = new URL(url);

    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return '';
    }

    return parsed.href;
  } catch {
    return '';
  }
}

// Input sanitization hook
function useSanitizedInput(initialValue = '') {
  const [value, setValue] = React.useState(initialValue);

  const handleChange = (e) => {
    const sanitized = DOMPurify.sanitize(e.target.value, {
      ALLOWED_TAGS: [],
      KEEP_CONTENT: true
    });

    setValue(sanitized);
  };

  return [value, handleChange];
}

// Usage
function CommentForm() {
  const [comment, handleCommentChange] = useSanitizedInput();

  const handleSubmit = async (e) => {
    e.preventDefault();

    await fetch('/api/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ comment })
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={handleCommentChange}
        placeholder="Enter comment"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export { SafeText, SafeHTML, SafeLink, useSanitizedInput };
javascript
// XSSSafeComponent.jsx
import React from 'react';
import DOMPurify from 'dompurify';

// Safe text rendering (React automatically escapes)
function SafeText({ text }) {
  return <div>{text}</div>;
}

// Sanitized HTML rendering
function SafeHTML({ html }) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a'],
    ALLOWED_ATTR: ['href']
  });

  return (
    <div
      dangerouslySetInnerHTML={{ __html: sanitized }}
    />
  );
}

// Safe URL attribute
function SafeLink({ href, children }) {
  const safeHref = sanitizeURL(href);

  return (
    <a
      href={safeHref}
      rel="noopener noreferrer"
      target="_blank"
    >
      {children}
    </a>
  );
}

function sanitizeURL(url) {
  try {
    const parsed = new URL(url);

    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return '';
    }

    return parsed.href;
  } catch {
    return '';
  }
}

// Input sanitization hook
function useSanitizedInput(initialValue = '') {
  const [value, setValue] = React.useState(initialValue);

  const handleChange = (e) => {
    const sanitized = DOMPurify.sanitize(e.target.value, {
      ALLOWED_TAGS: [],
      KEEP_CONTENT: true
    });

    setValue(sanitized);
  };

  return [value, handleChange];
}

// Usage
function CommentForm() {
  const [comment, handleCommentChange] = useSanitizedInput();

  const handleSubmit = async (e) => {
    e.preventDefault();

    await fetch('/api/comments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ comment })
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={handleCommentChange}
        placeholder="Enter comment"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

export { SafeText, SafeHTML, SafeLink, useSanitizedInput };

4. Content Security Policy

4. 内容安全策略(Content Security Policy)

javascript
// csp-config.js
const helmet = require('helmet');

function setupCSP(app) {
  app.use(helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],

      // Only allow scripts from trusted sources
      scriptSrc: [
        "'self'",
        "'nonce-RANDOM_NONCE'", // Use dynamic nonces
        "https://cdn.example.com"
      ],

      // Styles
      styleSrc: [
        "'self'",
        "'nonce-RANDOM_NONCE'",
        "https://fonts.googleapis.com"
      ],

      // No inline styles/scripts
      objectSrc: ["'none'"],
      baseUri: ["'self'"],

      // Report violations
      reportUri: ['/api/csp-violations']
    }
  }));

  // CSP violation reporter
  app.post('/api/csp-violations', (req, res) => {
    console.error('CSP Violation:', req.body);
    res.status(204).end();
  });
}

// Generate nonce for inline scripts
function generateNonce() {
  return require('crypto').randomBytes(16).toString('base64');
}

// Express middleware to add nonce
app.use((req, res, next) => {
  res.locals.nonce = generateNonce();
  next();
});

// In templates: <script nonce="<%= nonce %>">
javascript
// csp-config.js
const helmet = require('helmet');

function setupCSP(app) {
  app.use(helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],

      // Only allow scripts from trusted sources
      scriptSrc: [
        "'self'",
        "'nonce-RANDOM_NONCE'", // Use dynamic nonces
        "https://cdn.example.com"
      ],

      // Styles
      styleSrc: [
        "'self'",
        "'nonce-RANDOM_NONCE'",
        "https://fonts.googleapis.com"
      ],

      // No inline styles/scripts
      objectSrc: ["'none'"],
      baseUri: ["'self'"],

      // Report violations
      reportUri: ['/api/csp-violations']
    }
  }));

  // CSP violation reporter
  app.post('/api/csp-violations', (req, res) => {
    console.error('CSP Violation:', req.body);
    res.status(204).end();
  });
}

// Generate nonce for inline scripts
function generateNonce() {
  return require('crypto').randomBytes(16).toString('base64');
}

// Express middleware to add nonce
app.use((req, res, next) => {
  res.locals.nonce = generateNonce();
  next();
});

// In templates: <script nonce="<%= nonce %>">

Best Practices

最佳实践

✅ DO

✅ 建议

  • Encode output by default
  • Use templating engines
  • Implement CSP headers
  • Sanitize rich content
  • Validate URLs
  • Use HTTPOnly cookies
  • Regular security testing
  • Use secure frameworks
  • 默认对输出进行编码
  • 使用模板引擎
  • 部署CSP标头
  • 清理富文本内容
  • 验证URL
  • 使用HTTPOnly Cookie
  • 定期进行安全测试
  • 使用安全框架

❌ DON'T

❌ 禁止

  • Trust user input
  • Use innerHTML directly
  • Skip output encoding
  • Allow inline scripts
  • Use eval()
  • Mix contexts (HTML/JS)
  • 信任用户输入
  • 直接使用innerHTML
  • 跳过输出编码
  • 允许内联脚本
  • 使用eval()
  • 混合上下文(HTML/JS)

XSS Types

XSS攻击类型

  • Reflected: Immediate response
  • Stored: Persisted in database
  • DOM-based: Client-side manipulation
  • Mutation-based: Parser differences
  • 反射型:即时响应型攻击
  • 存储型:持久化到数据库的攻击
  • DOM型:客户端操作型攻击
  • 变异型:利用解析器差异的攻击

Context-Specific Encoding

特定上下文编码

  • HTML Content: HTML entity encoding
  • HTML Attribute: Attribute encoding
  • JavaScript: JavaScript escaping
  • URL: URL encoding
  • CSS: CSS escaping
  • HTML内容:HTML实体编码
  • HTML属性:属性编码
  • JavaScript:JavaScript转义
  • URL:URL编码
  • CSS:CSS转义

Resources

参考资源