Skip to content
On this page

内容安全策略 (CSP)

内容安全策略(Content Security Policy,CSP)是一种安全机制,通过声明式策略来控制浏览器可以加载和执行哪些资源,从而有效防范跨站脚本(XSS)、点击劫持等注入攻击。

CSP 基础概念

CSP 的作用

CSP 允许网站管理员定义:

  • 哪些源可以加载脚本
  • 哪些源可以加载样式
  • 哪些源可以加载图片
  • 哪些源可以发起网络请求
  • 等等

CSP 的工作原理

CSP 通过 HTTP 响应头或 HTML meta 标签发送给浏览器,浏览器根据策略决定是否加载和执行资源。

CSP 指令详解

基础指令

javascript
// CSP 指令映射
const cspDirectives = {
  // 默认策略指令
  defaultSrc: {
    description: '默认策略,当其他策略未指定时使用',
    example: ["'self'"]
  },
  
  // 脚本相关指令
  scriptSrc: {
    description: '定义 JavaScript 脚本的允许来源',
    example: ["'self'", "'unsafe-inline'", "'unsafe-eval'"]
  },
  
  // 样式相关指令
  styleSrc: {
    description: '定义 CSS 样式的允许来源',
    example: ["'self'", "'unsafe-inline'"]
  },
  
  // 图片相关指令
  imgSrc: {
    description: '定义图片的允许来源',
    example: ["'self'", "data:", "https:"]
  },
  
  // 字体相关指令
  fontSrc: {
    description: '定义字体文件的允许来源',
    example: ["'self'", "https:", "data:"]
  },
  
  // 网络请求指令
  connectSrc: {
    description: '定义 XMLHttpRequest、WebSocket 等连接的允许来源',
    example: ["'self'", "https://api.example.com"]
  },
  
  // 嵌入内容指令
  frameSrc: {
    description: '定义 frame 和 iframe 的允许来源',
    example: ["'self'", "https://trusted-embed.com"]
  },
  
  // 插件指令
  objectSrc: {
    description: '定义插件(如 Flash)的允许来源',
    example: ["'none'"] // 通常禁用
  }
};

高级指令

javascript
// 高级 CSP 指令
const advancedCspDirectives = {
  // 工作线程指令
  workerSrc: {
    description: '定义 Worker、SharedWorker、ServiceWorker 的允许来源',
    example: ["'self'"]
  },
  
  // 嵌入子资源指令
  childSrc: {
    description: '定义嵌入子资源(如 Worker、frame)的允许来源',
    example: ["'self'"]
  },
  
  // 框架祖先指令
  frameAncestors: {
    description: '定义哪些页面可以嵌入当前页面',
    example: ["'none'"] // 防止点击劫持
  },
  
  // 表单动作指令
  formAction: {
    description: '定义表单提交的允许目标',
    example: ["'self'"]
  },
  
  // 基础 URI 指令
  baseUri: {
    description: '定义 base 标签的允许来源',
    example: ["'self'"]
  },
  
  // 导航指令
  navigateTo: {
    description: '定义导航目标的允许来源',
    example: ["'self'"]
  },
  
  // 升级不安全请求指令
  upgradeInsecureRequests: {
    description: '自动将 HTTP 请求升级为 HTTPS',
    example: [] // 无值
  },
  
  // 报告 URI 指令
  reportUri: {
    description: '定义 CSP 违规报告的接收地址',
    example: ["/csp-report"]
  },
  
  // 媒体资源指令
  mediaSrc: {
    description: '定义音频、视频等媒体资源的允许来源',
    example: ["'self'"]
  }
};

CSP 源值详解

源值类型

javascript
// CSP 源值示例
const cspSourceValues = {
  // 同源策略
  self: {
    value: "'self'",
    description: '允许同源资源',
    example: 'https://example.com 可以加载 https://example.com/path'
  },
  
  // 数据 URL
  data: {
    value: 'data:',
    description: '允许 data: URL',
    example: '允许内联图片 data:image/png;base64,...'
  },
  
  // 空白源
  none: {
    value: "'none'",
    description: '不允许任何资源',
    example: 'object-src \'none\' 禁止所有插件'
  },
  
  // 内联资源
  unsafeInline: {
    value: "'unsafe-inline'",
    description: '允许内联脚本和样式',
    warning: '降低安全性,谨慎使用'
  },
  
  // 动态执行
  unsafeEval: {
    value: "'unsafe-eval'",
    description: '允许 eval 和动态代码执行',
    warning: '高风险,尽量避免使用'
  },
  
  // 非安全协议
  unsafeAllowRedirects: {
    value: "'unsafe-redirects'",
    description: '允许不安全的重定向'
  },
  
  // 哈希值
  hash: {
    value: "'sha256-abc123...'",
    description: '允许特定哈希值的内联脚本',
    example: 'script-src \'sha256-hashed-value\''
  },
  
  // 随机数
  nonce: {
    value: "'nonce-abc123'",
    description: '允许特定随机数的内联脚本',
    example: 'script-src \'nonce-unique-value\''
  }
};

// 源值验证函数
function validateCspSource(source) {
  const patterns = {
    self: /^'self'$/,
    none: /^'none'$/,
    unsafeInline: /^'unsafe-inline'$/,
    unsafeEval: /^'unsafe-eval'$/,
    data: /^data:$/,
    https: /^https:$/,
    http: /^http:$/,
    hash: /^'sha\d{1,3}-[a-zA-Z0-9+/=]{1,}$/,
    nonce: /^'nonce-[a-zA-Z0-9+/=]{1,}'$/
  };
  
  for (const [type, pattern] of Object.entries(patterns)) {
    if (pattern.test(source)) {
      return { valid: true, type };
    }
  }
  
  return { valid: false, type: 'unknown' };
}

CSP 实现示例

基础 CSP 配置

javascript
// 基础 CSP 策略
const basicCsp = {
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"], // 开发阶段允许内联样式
    imgSrc: ["'self'", "data:", "https:"],
    fontSrc: ["'self'", "https:", "data:"],
    connectSrc: ["'self'"],
    frameSrc: ["'none'"],
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    formAction: ["'self'"],
    baseUri: ["'self'"]
  },
  
  build() {
    return Object.entries(this.directives)
      .filter(([directive, sources]) => sources.length > 0)
      .map(([directive, sources]) => {
        const directiveName = directive
          .replace(/([A-Z])/g, '-$1')
          .toLowerCase();
        const sourceList = sources.join(' ');
        return `${directiveName} ${sourceList}`;
      })
      .join('; ');
  }
};

// 在 Express.js 中使用
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', basicCsp.build());
  next();
});

严格 CSP 配置

javascript
// 严格 CSP 策略(推荐用于生产环境)
const strictCsp = {
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: [
      "'self'",
      // 使用 nonce 策略而不是 'unsafe-inline'
    ],
    styleSrc: [
      "'self'",
      // 使用 nonce 策略而不是 'unsafe-inline'
    ],
    imgSrc: ["'self'", "data:", "blob:", "https:"],
    fontSrc: ["'self'", "https:", "data:"],
    connectSrc: ["'self'", "https://api.example.com"],
    mediaSrc: ["'self'"],
    objectSrc: ["'none'"],
    frameSrc: ["'none'"],
    frameAncestors: ["'none'"],
    formAction: ["'self'"],
    baseUri: ["'self'"],
    manifestSrc: ["'self'"],
    workerSrc: ["'self'"]
  },
  
  // 生成带 nonce 的策略
  withNonce(nonce) {
    const strictPolicy = { ...this.directives };
    strictPolicy.scriptSrc = [...strictPolicy.scriptSrc, `'nonce-${nonce}'`];
    strictPolicy.styleSrc = [...strictPolicy.styleSrc, `'nonce-${nonce}'`];
    
    return Object.entries(strictPolicy)
      .filter(([directive, sources]) => sources.length > 0)
      .map(([directive, sources]) => {
        const directiveName = directive
          .replace(/([A-Z])/g, '-$1')
          .toLowerCase();
        const sourceList = sources.join(' ');
        return `${directiveName} ${sourceList}`;
      })
      .join('; ');
  },
  
  // 生成带哈希的策略
  withHashes(hashes) {
    const strictPolicy = { ...this.directives };
    strictPolicy.scriptSrc = [
      ...strictPolicy.scriptSrc,
      ...hashes.scripts.map(hash => `'sha256-${hash}'`)
    ];
    strictPolicy.styleSrc = [
      ...strictPolicy.styleSrc,
      ...hashes.styles.map(hash => `'sha256-${hash}'`)
    ];
    
    return Object.entries(strictPolicy)
      .filter(([directive, sources]) => sources.length > 0)
      .map(([directive, sources]) => {
        const directiveName = directive
          .replace(/([A-Z])/g, '-$1')
          .toLowerCase();
        const sourceList = sources.join(' ');
        return `${directiveName} ${sourceList}`;
      })
      .join('; ');
  }
};

// 带 nonce 的 CSP 中间件
function cspWithNonce(req, res, next) {
  // 生成随机 nonce
  const nonce = require('crypto').randomBytes(16).toString('hex');
  
  // 将 nonce 添加到响应 locals,以便模板引擎使用
  res.locals.nonce = nonce;
  
  // 设置 CSP 头部
  const cspPolicy = strictCsp.withNonce(nonce);
  res.setHeader('Content-Security-Policy', cspPolicy);
  
  next();
}

app.use(cspWithNonce);

CSP 报告机制

CSP 违规报告

javascript
// CSP 违规报告处理
app.use(express.json({ type: 'application/csp-report' }));

app.post('/csp-report', (req, res) => {
  const report = req.body['csp-report'];
  
  if (report) {
    console.error('CSP Violation Report:', {
      documentUri: report.document_uri,
      violatedDirective: report.violated_directive,
      originalPolicy: report.original_policy,
      blockedUri: report.blocked_uri,
      sourceFile: report.source_file,
      lineNumber: report.line_number,
      columnNumber: report.column_number,
      effectiveDirective: report.effective_directive,
      statusCode: report.status_code,
      scriptSample: report.script_sample,
      timestamp: new Date().toISOString()
    });
    
    // 可以将报告存储到数据库或发送警报
    storeCspViolation(report);
  }
  
  res.status(204).end();
});

// 存储 CSP 违规的函数
async function storeCspViolation(report) {
  try {
    // 这里可以连接到数据库或日志服务
    console.log('Storing CSP violation:', {
      blockedUri: report.blocked_uri,
      violatedDirective: report.violated_directive,
      documentUri: report.document_uri,
      timestamp: new Date().toISOString()
    });
  } catch (error) {
    console.error('Failed to store CSP violation:', error);
  }
}

报告 URI 配置

javascript
// 带报告的 CSP 配置
const cspWithReporting = {
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    reportUri: ["/csp-report"],
    // 或使用现代的 report-to 指令
    reportTo: ['csp-endpoint']
  },
  
  endpoints: {
    'csp-endpoint': {
      url: '/csp-report',
      includeCspReportOnly: true
    }
  },
  
  build() {
    const directives = [];
    
    for (const [directive, sources] of Object.entries(this.directives)) {
      if (sources.length > 0) {
        const directiveName = directive
          .replace(/([A-Z])/g, '-$1')
          .toLowerCase();
        const sourceList = sources.join(' ');
        directives.push(`${directiveName} ${sourceList}`);
      }
    }
    
    return directives.join('; ');
  }
};

// 设置报告端点
app.use((req, res, next) => {
  res.setHeader('Reporting-Endpoints', 
    'csp-endpoint="/csp-report", default="/csp-report"'
  );
  res.setHeader('Content-Security-Policy', cspWithReporting.build());
  next();
});

框架集成

Express.js 中的 CSP

javascript
const helmet = require('helmet');

// 使用 Helmet 配置 CSP
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    fontSrc: ["'self'", "https:", "data:"],
    connectSrc: ["'self'", "https://api.example.com"],
    frameSrc: ["'none'"],
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    formAction: ["'self'"],
    baseUri: ["'self'"]
  },
  reportOnly: false // 是否只报告不阻止
}));

// 自定义 CSP 中间件
function customCspMiddleware(policy) {
  return (req, res, next) => {
    res.setHeader('Content-Security-Policy', policy);
    next();
  };
}

// 动态 CSP 策略
function createDynamicCsp(userRole = 'default') {
  const policies = {
    default: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"]
    },
    admin: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-eval'"], // 管理员可能需要动态执行
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:", "http:"],
      connectSrc: ["'self'", "https://api.example.com", "https://admin-api.example.com"]
    }
  };
  
  const userPolicy = policies[userRole] || policies.default;
  
  return Object.entries(userPolicy)
    .map(([directive, sources]) => {
      const directiveName = directive
        .replace(/([A-Z])/g, '-$1')
        .toLowerCase();
      const sourceList = sources.join(' ');
      return `${directiveName} ${sourceList}`;
    })
    .join('; ');
}

// 根据用户角色设置 CSP
app.use((req, res, next) => {
  const userRole = req.user?.role || 'default';
  const cspPolicy = createDynamicCsp(userRole);
  res.setHeader('Content-Security-Policy', cspPolicy);
  next();
});

React 应用中的 CSP 支持

javascript
// React 应用的 CSP 兼容性处理
// 1. 避免内联事件处理器
// ❌ 错误:onClick="handleClick()"
// ✅ 正确:<button onClick={handleClick}>

// 2. 使用 React 的 dangerouslySetInnerHTML 时要小心
function SafeHtmlRenderer({ htmlContent, allowedDomains }) {
  // 在服务端预处理 HTML 内容
  const sanitizedHtml = sanitizeHtml(htmlContent, {
    allowedTags: ['p', 'br', 'strong', 'em', 'ul', 'li', 'h1', 'h2', 'h3'],
    allowedAttributes: {
      a: ['href', 'title'],
      img: ['src', 'alt']
    },
    allowedSchemes: ['https', 'http', 'data'],
    allowedSchemesByTag: {
      img: ['https', 'http', 'data']
    }
  });
  
  return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
}

// 3. 动态脚本加载的 CSP 友好方式
async function loadScript(src) {
  return new Promise((resolve, reject) => {
    // 如果有 nonce,使用它
    const nonce = document.querySelector('meta[name="csp-nonce"]')?.content;
    
    const script = document.createElement('script');
    if (nonce) {
      script.nonce = nonce;
    }
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

CSP 调试和测试

CSP 违规检测

javascript
// CSP 违规检测工具
class CspViolationDetector {
  constructor() {
    this.violations = [];
    this.init();
  }
  
  init() {
    // 监听 CSP 违规事件
    if (window.addEventListener) {
      window.addEventListener('securitypolicyviolation', (e) => {
        this.handleViolation(e);
      });
    }
  }
  
  handleViolation(event) {
    const violation = {
      blockedUri: event.blockedURI,
      directive: event.violatedDirective,
      effectiveDirective: event.effectiveDirective,
      originalPolicy: event.originalPolicy,
      sourceFile: event.sourceFile,
      lineNumber: event.lineNumber,
      columnNumber: event.columnNumber,
      timestamp: new Date().toISOString()
    };
    
    this.violations.push(violation);
    console.warn('CSP Violation:', violation);
    
    // 可以发送到分析服务
    this.reportViolation(violation);
  }
  
  reportViolation(violation) {
    // 发送违规报告到服务器
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/csp-report', JSON.stringify({
        'csp-report': violation
      }));
    } else {
      // 降级处理
      fetch('/csp-report', {
        method: 'POST',
        body: JSON.stringify({ 'csp-report': violation }),
        headers: { 'Content-Type': 'application/json' }
      }).catch(err => console.error('Failed to report CSP violation:', err));
    }
  }
  
  getViolations() {
    return this.violations;
  }
  
  clearViolations() {
    this.violations = [];
  }
}

// 初始化检测器
if (typeof window !== 'undefined') {
  window.cspDetector = new CspViolationDetector();
}

CSP 策略生成工具

javascript
// CSP 策略生成器
class CspPolicyBuilder {
  constructor() {
    this.directives = {};
  }
  
  addDirective(name, sources = []) {
    const normalizedSources = Array.isArray(sources) ? sources : [sources];
    const directiveName = name.replace(/([A-Z])/g, '-$1').toLowerCase();
    
    if (!this.directives[directiveName]) {
      this.directives[directiveName] = [];
    }
    
    this.directives[directiveName] = [
      ...this.directives[directiveName],
      ...normalizedSources
    ];
    
    return this;
  }
  
  setDefaultSrc(sources) {
    return this.addDirective('defaultSrc', sources);
  }
  
  setScriptSrc(sources) {
    return this.addDirective('scriptSrc', sources);
  }
  
  setStyleSrc(sources) {
    return this.addDirective('styleSrc', sources);
  }
  
  setImgSrc(sources) {
    return this.addDirective('imgSrc', sources);
  }
  
  setConnectSrc(sources) {
    return this.addDirective('connectSrc', sources);
  }
  
  setReportUri(uri) {
    return this.addDirective('reportUri', uri);
  }
  
  build() {
    return Object.entries(this.directives)
      .filter(([_, sources]) => sources.length > 0)
      .map(([directive, sources]) => {
        const uniqueSources = [...new Set(sources)];
        return `${directive} ${uniqueSources.join(' ')}`;
      })
      .join('; ');
  }
  
  // 生成报告模式的策略(只报告不阻止)
  buildReportOnly() {
    const policy = this.build();
    return {
      policy,
      header: 'Content-Security-Policy-Report-Only'
    };
  }
}

// 使用示例
const cspBuilder = new CspPolicyBuilder();
const cspPolicy = cspBuilder
  .setDefaultSrc(["'self'"])
  .setScriptSrc(["'self'", "'unsafe-inline'"]) // 开发阶段
  .setStyleSrc(["'self'", "'unsafe-inline'"])  // 开发阶段
  .setImgSrc(["'self'", "data:", "https:"])
  .setConnectSrc(["'self'", "https://api.example.com"])
  .setReportUri(["/csp-report"])
  .build();

console.log('Generated CSP:', cspPolicy);

CSP 最佳实践

1. 渐进式实施

javascript
// 分阶段 CSP 实施策略
const cspImplementation = {
  phase1: {
    name: 'Monitor Only',
    policy: new CspPolicyBuilder()
      .setDefaultSrc(["'self'"])
      .setReportUri(["/csp-report"])
      .build(),
    header: 'Content-Security-Policy-Report-Only'
  },
  
  phase2: {
    name: 'Basic Protection',
    policy: new CspPolicyBuilder()
      .setDefaultSrc(["'self'"])
      .setScriptSrc(["'self'", "'unsafe-inline'"]) // 临时允许
      .setStyleSrc(["'self'", "'unsafe-inline'"])  // 临时允许
      .setImgSrc(["'self'", "data:", "https:"])
      .setReportUri(["/csp-report"])
      .build(),
    header: 'Content-Security-Policy-Report-Only'
  },
  
  phase3: {
    name: 'Strict Policy',
    policy: new CspPolicyBuilder()
      .setDefaultSrc(["'self'"])
      .setScriptSrc(["'self'"]) // 移除 'unsafe-inline'
      .setStyleSrc(["'self'"])  // 移除 'unsafe-inline'
      .setImgSrc(["'self'", "data:", "https:"])
      .setConnectSrc(["'self'", "https://api.example.com"])
      .setFrameAncestors(["'none'"])
      .setObjectSrc(["'none'"])
      .setFormAction(["'self'"])
      .setBaseUri(["'self'"])
      .build(),
    header: 'Content-Security-Policy'
  }
};

// 根据实施阶段设置 CSP
function setCspByPhase(req, res, next) {
  const phase = process.env.CSP_PHASE || 'phase1';
  const config = cspImplementation[phase];
  
  if (config) {
    res.setHeader(config.header, config.policy);
  }
  
  next();
}

app.use(setCspByPhase);

2. 环境特定配置

javascript
// 环境特定 CSP 配置
function getCspConfig(environment = process.env.NODE_ENV) {
  const configs = {
    development: {
      // 开发环境:允许更多灵活性
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:", "http:"],
        fontSrc: ["'self'", "https:", "http:", "data:"],
        connectSrc: ["'self'", "ws:", "http:", "https:"],
        frameSrc: ["'self'", "https:"],
        reportUri: ["/csp-report"]
      },
      reportOnly: true
    },
    
    staging: {
      // 预发布环境:接近生产环境的策略
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'", "https:", "data:"],
        connectSrc: ["'self'", "https://api.staging.example.com"],
        frameSrc: ["'none'"],
        objectSrc: ["'none'"],
        frameAncestors: ["'none'"],
        formAction: ["'self'"],
        baseUri: ["'self'"],
        reportUri: ["/csp-report"]
      },
      reportOnly: true
    },
    
    production: {
      // 生产环境:最严格的策略
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'", "https:", "data:"],
        connectSrc: ["'self'", "https://api.example.com"],
        mediaSrc: ["'self'"],
        objectSrc: ["'none'"],
        frameSrc: ["'none'"],
        frameAncestors: ["'none'"],
        formAction: ["'self'"],
        baseUri: ["'self'"],
        upgradeInsecureRequests: true
      },
      reportOnly: false
    }
  };
  
  return configs[environment] || configs.production;
}

// 应用环境特定 CSP
app.use((req, res, next) => {
  const config = getCspConfig();
  const policy = Object.entries(config.directives)
    .filter(([_, sources]) => sources.length > 0)
    .map(([directive, sources]) => {
      const directiveName = directive
        .replace(/([A-Z])/g, '-$1')
        .toLowerCase();
      const sourceList = sources.join(' ');
      return `${directiveName} ${sourceList}`;
    })
    .join('; ');
  
  const headerName = config.reportOnly 
    ? 'Content-Security-Policy-Report-Only' 
    : 'Content-Security-Policy';
  
  res.setHeader(headerName, policy);
  next();
});

总结

CSP 是防范 XSS 等注入攻击的有效工具:

  1. 理解指令 - 熟悉各种 CSP 指令的作用
  2. 合理配置 - 根据应用需求配置适当的策略
  3. 使用 nonce/hash - 避免使用 'unsafe-inline'
  4. 实施监控 - 设置报告机制监控违规
  5. 分阶段实施 - 从宽松到严格逐步推进
  6. 环境区分 - 不同环境使用不同策略

正确实施 CSP 可以显著提高 Web 应用的安全性。