吃透 CSP:现代 Web 安全的核心机制

作为 Web 开发工程师,我们时刻面临着各种安全威胁,其中跨站脚本攻击(XSS)是最常见也是危害最大的漏洞之一。攻击者通过注入恶意脚本,可以窃取用户敏感信息、劫持会话甚至篡改网页内容。传统的防御手段(如输入验证和输出编码)虽然重要,但往往难以做到滴水不漏。这时,浏览器提供的一项重要安全特性——Content Security Policy (CSP)——就显得尤为关键。那么今天,让我们先从理论层面好好来梳理一下 CSP,看看它到底有哪些约束来保障我们的 web 内容的安全性,下一期我们再从代码演示上来分别展示。

CSP 本质上是建立一个内容安全白名单,通过 HTTP 头或 标签告知浏览器哪些外部资源(脚本、样式、图片、字体、框架等)允许被加载和执行。如果资源的来源不在白名单内,浏览器就会拒绝加载,从而有效地阻止了大部分 XSS 攻击以及其他类型的内容注入攻击。

如下图,抖音官网的响应头中关于 CSP 相关的配置

为什么需要 CSP?

想象一下,你的网站有一个留言板功能。如果没有对用户输入进行严格的过滤,攻击者可能会提交包含恶意 ) 或内联样式 (, style="...")。 极不推荐使用,因为它会引入 XSS 风险。

  • 'unsafe-eval': 允许使用 eval(), setTimeout("..."), setInterval("...") 等从字符串执行代码的方法。 极不推荐使用,因为它也引入 XSS 风险。
  • 'nonce-': 允许带有特定 nonce 属性的内联脚本或样式。服务器在每次响应时生成一个唯一的 nonce 值,并将其添加到 CSP 头和相应的
  • 'sha256-': 允许具有特定哈希值的内联脚本或样式。你可以计算内联脚本或样式的哈希值(支持 SHA256, SHA384, SHA512),并将其添加到 CSP 头中。
    • 示例 CSP 头: script-src 'sha256-abcdefg...'
    • 示例 HTML: (你需要计算 alert('Hello, CSP!'); 这段代码的 SHA256 哈希值)
  • 'strict-dynamic': 一个强大的指令(通常与 nonce 或 hash 一起使用)。如果一个脚本通过 nonce 或 hash 被允许执行,那么这个脚本加载的其他脚本(例如通过 document.createElement('script') 创建并添加到 DOM 中)也会被信任并允许执行,而无需显式地在 CSP 中列出这些脚本的来源。这极大地简化了对复杂应用(如 SPA)的 CSP 管理,同时保持了安全性。
    • 示例 CSP 头: script-src 'nonce-abc123xyz' 'strict-dynamic'
    • 示例 HTML: (即使 example.com 不在 script-src 中,dynamic.js 也会被允许加载)
  • 使用 Express 实现一个 CSP Demo

    下面我们将创建一个简单的 Express 应用来演示如何设置 CSP 头部以及不同策略的效果。

    项目结构:

    csp-demo/
    ├── package.json
    ├── app.js
    └── public/
        ├── index.html
        ├── safe-script.js
    

    1. 初始化项目并安装 Express 和 body-parser (用于接收报告):

    mkdir csp-demo
    cd csp-demo
    npm init -y
    npm install express body-parser
    

    2. 创建 public/index.html:

    
    
    
        
        
        CSP 示例
    
        
        
    
        
        
        
    
        
        
    
        
        
    
    
    
        

    内容安全策略(CSP)演示页面

    打开浏览器开发者工具查看 CSP 违规报告。

    占位图片

    3. 创建 public/safe-script.js:

    console.log('这是一个来自同源的安全脚本。');
    

    4. 创建 app.js:

    const express = require('express');
    const path = require('path');
    const bodyParser = require('body-parser');
    
    const app = express();
    const port = 3000;
    
    // 使用 body-parser 中间件来解析请求体,用于处理 CSP 报告
    // 针对 JSON 类型和 application/csp-report 类型的请求体进行解析
    app.use(bodyParser.json({
        type: ['json', 'application/csp-report'] // 特别用于处理 CSP 的违规报告
    }));
    
    
    // 自定义中间件:设置 Content-Security-Policy(内容安全策略)响应头
    app.use((req, res, next) => {
        // 定义你的 CSP 策略
        // 当前策略规定:
        // - 默认资源加载来源为 'self'(即同源)
        // - 脚本允许从 'self' 和 cdnjs.cloudflare.com 加载
        // - 样式表允许从 'self' 加载,并允许内联样式('unsafe-inline'),仅用于演示,生产环境应避免使用
        // - 图片资源允许从 'self' 和 https://via.placeholder.com 加载
        // - 所有违反策略的行为将报告到 /report 接口
        const cspPolicy = `
            default-src 'self';
            script-src 'self' https://cdnjs.cloudflare.com 'sha256-WnZRYRws9lJmeyKcnwV8cR+ycNmLoVQQPANm6GNlsUk=';
            style-src 'self' 'unsafe-inline';
            img-src 'self' https://placehold.co;
            report-uri /report;
        `.replace(/\s+/g, ' ').trim(); // 清理多余的空白字符
    
        // 使用 Content-Security-Policy 响应头来 **强制执行** 策略
        res.setHeader('Content-Security-Policy', cspPolicy);
        next();
    });
    
    
    // 从 'public' 目录提供静态文件(HTML、CSS、JS 等)
    app.use(express.static(path.join(__dirname, 'public')));
    // 接收 CSP 违规报告的接口
    app.post('/report', (req, res) => {
        console.log('收到 CSP 违规报告:');
        console.log(JSON.stringify(req.body, null, 2)); // 将报告内容格式化后打印到控制台
        res.sendStatus(204); // 返回 204 No Content 表示成功接收但无内容返回
    });
    
    // 启动服务器
    app.listen(port, () => {
        console.log(`CSP 示例应用正在运行在 http://localhost:${port}`);
        console.log('请在浏览器中打开 http://localhost:3000 并查看控制台输出。');
    });
    

    5. 运行应用:

    node app.js
    

    打开浏览器访问 http://localhost:3000。打开开发者工具的 Console (控制台) 和 Network (网络) 面板。

    观察现象:

    • 根据 script-src 'self' https://cdnjs.cloudflare.com; 策略:
      • 内联脚本("这是一个内联脚本.")会被阻止,除非策略中包含 'unsafe-inline' 或使用了 nonce/hash。

      • 同源脚本 (safe-script.js) 会被允许加载。

      • 来自 cdnjs 的 jQuery 脚本会被允许加载。

      • console.log('这是一个由 hash 判断的脚本。'); 会被允许加载

    • 根据 style-src 'self' 'unsafe-inline'; 策略:
      • 内联样式 (body { ... }) 会被允许,因为包含了 'unsafe-inline'。如果去掉 'unsafe-inline',内联样式就会被阻止。
    • 根据 img-src 'self' https://via.placeholder.com; 策略:
      • 来自 https://placehold.co 的图片会被允许加载。如果去掉这个来源,图片就会被阻止。
    • 内联事件处理函数 (onclick="alert('...')) 通常会被 script-src 阻止,即使有 'unsafe-inline'。它们需要更宽松的策略或使用其他技术(如通过 JavaScript 添加事件监听器)。
    • 在 Console 中,你会看到关于 CSP 违规的警告或错误信息。

    • 如果你的策略包含了 report-uri /report; 并且有违规发生,服务器端的控制台会打印出接收到的违规报告的 JSON 数据。

    你可以修改 app.js 中的 cspPolicy 来实验不同的 CSP 策略,观察它们对页面行为的影响。例如,去掉 'unsafe-inline',看看内联脚本和样式是否被阻止。

    构建工具 (Webpack/Vite) 的最佳实践

    现代 Web 开发通常使用构建工具(如 Webpack 或 Vite)来打包、优化和处理资源。构建过程可能会产生一些 CSP 需要特别关注的问题:

    • 内联代码: 构建工具(尤其是 Webpack 的一些插件)可能会生成一些内联的运行时脚本或小的 CSS 块。这些内联代码如果没有被 CSP 允许,会导致应用无法正常运行。
    • 哈希文件名: 构建工具常常为了缓存优化,给输出文件添加哈希(例如 bundle.abcdef.js)。CSP 策略通常是基于来源路径,这与哈希文件名不冲突。

    解决方案:

    1. 最小化内联代码: 尽量减少构建工具生成的内联脚本和样式。
    2. 使用 Nonce 或 Hash:
      • Nonce: 这是处理动态生成的内联脚本(如运行时代码、动态导入的启动脚本)的最佳方式。
        • 实现: 服务器在每个请求时生成一个唯一的 base64 编码的 Nonce 值。将这个 Nonce 值添加到 CSP 响应头的 script-src 和/或 style-src 指令中,例如 script-src 'nonce-YOUR_NONCE_VALUE' 'self' ...;
        • 同时,将这个 Nonce 值传递给你的前端模板引擎,将它作为 nonce 属性添加到构建工具生成的内联

    你可能感兴趣的:(前端,安全,学习,javascript,科技,计算机,互联网)