XSS防御完全指南:从输入到输出的全链路防护

2025-01-16 | 22分钟阅读 | 防御方法

一、XSS防御核心原则

XSS防御遵循"纵深防御"策略,包含多个层次:

防御三原则

1. 输入验证:永远不要信任用户输入
2. 输出编码:在输出到页面前进行适当编码
3. 纵深防御:使用多层防护机制

1.1 为什么需要多层防御?

二、输入验证与过滤

2.1 白名单验证(推荐)

只接受符合预期格式的输入:

// PHP示例
function validateUsername($username) {
    // 只允许字母、数字和下划线,长度3-20
    if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
        throw new Exception('无效的用户名格式');
    }
    return $username;
}

// 验证邮箱
function validateEmail($email) {
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new Exception('无效的邮箱地址');
    }
    return $email;
}
// JavaScript客户端验证
function validateInput(input, type) {
    const patterns = {
        username: /^[a-zA-Z0-9_]{3,20}$/,
        email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        phone: /^1[3-9]\d{9}$/,
        url: /^https?:\/\/.+/
    };
    
    return patterns[type]?.test(input) || false;
}

2.2 黑名单过滤(不推荐作为唯一防御)

// 移除危险字符(容易被绕过)
function removeXSS($input) {
    $dangerous = ['<script>', '</script>', 'javascript:', 'onerror='];
    return str_ireplace($dangerous, '', $input);
}

// 更好的方式是使用HTML Purifier等库

2.3 内容长度限制

// 限制输入长度
function limitLength(input, maxLength = 200) {
    if (input.length > maxLength) {
        throw new Error('输入超过最大长度限制');
    }
    return input;
}

三、输出编码策略

3.1 HTML上下文编码

在HTML标签之间输出时:

// PHP
function encodeHTML($str) {
    return htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// 使用示例
echo '<div>' . encodeHTML($userInput) . '</div>';
// JavaScript
function encodeHTML(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
}

// 或使用现成方法
function escapeHTML(str) {
    return str
        .replace(/&/g, '&')
        .replace(//g, '>')
        .replace(/"/g, '"')
        .replace(/'/g, ''')
        .replace(/\//g, '/');
}

3.2 HTML属性上下文编码

<!-- 正确做法 -->
<div title="<?php echo encodeHTMLAttribute($userInput); ?>"></div>

<!-- 错误做法 -->
<div title="<?php echo $userInput; ?>"></div>
function encodeHTMLAttribute($str) {
    // 除了HTML编码,还要处理属性特殊字符
    return htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

3.3 JavaScript上下文编码

<script>
// 错误做法
var userInput = "<?php echo $userInput; ?>";

// 正确做法
var userInput = <?php echo json_encode($userInput, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>;
</script>
// JavaScript中安全地插入动态内容
function safeInnerHTML(element, html) {
    // 使用textContent代替innerHTML
    element.textContent = html;
}

// 或使用DOMPurify库
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(html);

3.4 URL上下文编码

// PHP
$safeURL = 'https://example.com/search?q=' . urlencode($userInput);

// 验证URL协议
function validateURL($url) {
    $parsed = parse_url($url);
    if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
        throw new Exception('不允许的URL协议');
    }
    return $url;
}
// JavaScript
const safeURL = 'https://example.com/search?q=' + encodeURIComponent(userInput);

// 验证URL
function isValidURL(url) {
    try {
        const parsed = new URL(url);
        return ['http:', 'https:'].includes(parsed.protocol);
    } catch {
        return false;
    }
}

3.5 CSS上下文编码

<!-- 避免在CSS中使用用户输入 -->
<style>
.user-color {
    /* 不要这样做 */
    background: <?php echo $userInput; ?>;
}
</style>

<!-- 如果必须使用,严格验证 -->
<?php
function validateColor($color) {
    if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
        return '#000000'; // 默认颜色
    }
    return $color;
}
?>

四、内容安全策略(CSP)

4.1 CSP基础配置

<!-- 最严格的CSP -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'">
// PHP设置CSP头
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-" . $nonce . "'; style-src 'self' 'nonce-" . $nonce . "'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'");

4.2 使用Nonce的CSP

<?php
// 生成随机nonce
$nonce = base64_encode(random_bytes(16));

// 设置CSP头
header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic'");
?>

<!-- 在script标签中使用nonce -->
<script nonce="<?php echo $nonce; ?>">
    console.log('这个脚本会被执行');
</script>

<!-- 没有nonce的脚本会被阻止 -->
<script>
    console.log('这个脚本会被CSP阻止');
</script>

4.3 CSP报告

// 设置CSP报告端点
header("Content-Security-Policy: default-src 'self'; report-uri /csp-report; report-to csp-endpoint");
header("Report-To: {\"group\":\"csp-endpoint\",\"max_age\":10886400,\"endpoints\":[{\"url\":\"/csp-report\"}]}");
// 处理CSP报告
// /csp-report 端点
$report = json_decode(file_get_contents('php://input'), true);

if ($report) {
    // 记录违规信息
    error_log('CSP Violation: ' . json_encode($report));
    
    // 可以发送到安全监控系统
    sendToSecurityMonitor($report);
}

CSP最佳实践

1. 从严格策略开始,逐步放宽
2. 使用nonce或hash而非'unsafe-inline'
3. 启用报告功能监控违规
4. 定期审查和更新CSP策略

五、Cookie安全配置

5.1 HttpOnly和Secure标志

// PHP设置安全Cookie
setcookie(
    'session_id',
    $sessionId,
    [
        'expires' => time() + 3600,
        'path' => '/',
        'domain' => '.example.com',
        'secure' => true,      // 仅HTTPS
        'httponly' => true,    // 禁止JS访问
        'samesite' => 'Strict' // 防CSRF
    ]
);
// Express.js示例
app.use(session({
    secret: 'your-secret-key',
    cookie: {
        secure: true,
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 3600000
    }
}));

5.2 SameSite Cookie

六、框架级别的防御

6.1 使用模板引擎的自动转义

<!-- Twig (PHP) -->
{{ userInput }}  {# 自动HTML转义 #}
{{ userInput|raw }}  {# 禁用转义(危险!) #}
// React自动转义
function UserProfile({ name }) {
    return <div>{name}</div>;  // 自动转义
}

// 危险的做法(不推荐)
function DangerousComponent({ html }) {
    return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

6.2 框架内置的XSS防护

// Angular自动转义
<div>{{ userInput }}</div>  <!-- 自动转义 -->

// Vue.js自动转义
<div>{{ userInput }}</div>  <!-- 自动转义 -->
<div v-html="userInput"></div>  <!-- 不转义,危险! -->

6.3 使用安全的API

// 使用textContent代替innerHTML
element.textContent = userInput;  // 安全
element.innerHTML = userInput;    // 危险

// 使用setAttribute代替直接赋值
element.setAttribute('title', userInput);  // 相对安全
element.title = userInput;                 // 也可以

// 避免eval和Function构造器
eval(userInput);  // 极度危险
new Function(userInput)();  // 极度危险

// 使用JSON.parse代替eval
const data = JSON.parse(jsonString);  // 安全

七、完整的XSS防御清单

开发阶段

☑ 对所有用户输入进行验证
☑ 在适当的上下文进行输出编码
☑ 使用自动转义的模板引擎
☑ 避免使用innerHTML、eval等危险API
☑ 实施严格的CSP策略
☑ 配置安全的Cookie属性
☑ 使用HTTPS传输敏感数据

测试阶段

☑ 进行XSS安全测试
☑ 使用自动化扫描工具
☑ 人工审查关键代码
☑ 测试CSP配置是否生效
☑ 验证所有输入输出点

部署阶段

☑ 启用WAF(Web应用防火墙)
☑ 配置安全响应头
☑ 定期更新框架和依赖库
☑ 监控安全日志
☑ 实施入侵检测系统

八、不同场景的防御策略

8.1 富文本编辑器

// 使用DOMPurify清理HTML
import DOMPurify from 'dompurify';

function sanitizeRichText(html) {
    return DOMPurify.sanitize(html, {
        ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a'],
        ALLOWED_ATTR: ['href', 'title', 'target'],
        ALLOW_DATA_ATTR: false
    });
}

8.2 Markdown渲染

// 使用marked + DOMPurify
import marked from 'marked';
import DOMPurify from 'dompurify';

function renderMarkdown(markdown) {
    const html = marked(markdown);
    return DOMPurify.sanitize(html);
}

8.3 JSON API

// 设置正确的Content-Type
header('Content-Type: application/json');

// 输出JSON数据
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);

九、总结

XSS防御是一个系统工程,需要在多个层面实施防护:

  1. 输入层:验证和过滤用户输入
  2. 处理层:安全地处理和存储数据
  3. 输出层:根据上下文进行适当编码
  4. HTTP层:配置安全响应头和Cookie
  5. 客户端层:使用CSP等浏览器安全特性

记住:永远不要信任用户输入,永远进行适当的输出编码!

上一篇:Cookie窃取实战 返回知识库 下一篇:CSP配置实战指南