WordPress安全:如何利用`Content Security Policy (CSP)`增强防护?

WordPress 安全:利用 Content Security Policy (CSP) 增强防护

各位朋友,大家好。今天我们来聊聊 WordPress 安全中一个非常重要的概念:Content Security Policy,简称 CSP。这是一个强大的安全工具,可以帮助我们防御各种 Web 攻击,特别是跨站脚本攻击 (XSS)。

什么是 CSP?

CSP 本质上是一个 HTTP 响应头,它告诉浏览器哪些资源(例如脚本、样式表、图像等)可以加载,以及这些资源可以从哪些来源加载。通过明确地定义一个允许加载资源的“白名单”,CSP 可以防止浏览器加载未经授权的、潜在恶意的资源,从而减少 XSS 攻击的风险。

为什么我们需要 CSP?

传统的 XSS 防御方法,例如输入验证和输出编码,虽然重要,但并非万无一失。攻击者总能找到新的方法绕过这些防御措施。CSP 提供了一种额外的安全层,即使攻击者成功注入了恶意脚本,CSP 也可以阻止浏览器执行该脚本,从而保护用户和网站的安全。

CSP 的基本语法

CSP 的语法基于指令 (directive) 和源表达式 (source expression)。指令定义了要控制的资源类型,源表达式定义了允许加载这些资源的来源。

一个简单的 CSP 示例:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:;

这个 CSP 头告诉浏览器:

  • default-src 'self': 默认情况下,只允许从当前域名 (self) 加载资源。
  • script-src 'self' 'unsafe-inline' 'unsafe-eval': 允许从当前域名加载脚本,也允许内联脚本 ('unsafe-inline') 和使用 eval() 函数执行的脚本 ('unsafe-eval')。
  • img-src 'self' data:: 允许从当前域名加载图像,也允许使用 data URI 格式的图像。

CSP 指令详解

以下是一些常用的 CSP 指令:

指令 描述
default-src 定义所有类型资源的默认来源。如果其他指令没有明确定义来源,则使用 default-src
script-src 定义允许加载 JavaScript 脚本的来源。
style-src 定义允许加载 CSS 样式表的来源。
img-src 定义允许加载图像的来源。
font-src 定义允许加载字体的来源。
connect-src 定义允许使用 XMLHttpRequestFetchWebSocket 等 API 连接的来源。
media-src 定义允许加载音频和视频文件的来源。
object-src 定义允许加载 <object><embed><applet> 元素的来源。(不建议使用这些元素,因为它们存在安全风险。)
frame-src 定义允许加载 <frame><iframe> 元素的来源。
child-src frame-src 的别名,用于定义允许加载 Web Workers 和嵌套浏览上下文的来源。 (在某些浏览器中,frame-srcchild-src 的行为可能不同,建议同时设置这两个指令以获得最佳兼容性。)
form-action 定义允许提交表单的 URL。
base-uri 定义允许在 <base> 元素中使用的 URL。
sandbox 为请求的资源启用沙箱。
report-uri 指定一个 URL,浏览器会将 CSP 违反报告发送到该 URL。 (已被 report-to 指令取代,但为了兼容性,建议同时使用这两个指令。)
report-to 指定一个命名的端点,浏览器会将 CSP 违反报告发送到该端点。 需要在 HTTP 响应头中配置 Report-To 头来定义端点。
worker-src 定义允许加载 Web Workers 的来源。
prefetch-src 定义允许预加载的资源的来源。
navigate-to 定义允许导航到的 URL。 (实验性功能,可能不被所有浏览器支持。)
require-trusted-types-for 控制哪些代码可以调用使用 Trusted Types API 的函数。 (Trusted Types API 旨在防止 DOM-based XSS 攻击。)
trusted-types 定义允许创建 Trusted Types 的策略。 (Trusted Types API 旨在防止 DOM-based XSS 攻击。)
require-sri-for 要求对指定的资源类型使用 Subresource Integrity (SRI) 校验。 (SRI 是一种安全特性,允许浏览器验证从 CDN 或其他第三方来源加载的文件是否被篡改。)

CSP 源表达式详解

源表达式定义了允许加载资源的来源。以下是一些常用的源表达式:

源表达式 描述
'self' 允许从当前域名加载资源。
'none' 阻止加载任何资源。
'unsafe-inline' 允许使用内联 JavaScript 脚本和 CSS 样式。 (不建议使用,因为它会增加 XSS 攻击的风险。)
'unsafe-eval' 允许使用 eval() 函数执行 JavaScript 代码。 (不建议使用,因为它会增加 XSS 攻击的风险。)
'strict-dynamic' 允许通过已信任的脚本 (例如,使用 nonce 或 hash 标记的脚本) 加载的其他脚本。 (需要与 'nonce-''hash-' 配合使用。)
'unsafe-hashes' 允许使用特定的内联事件处理程序 (例如,onclick)。 (不建议使用,因为它会增加 XSS 攻击的风险。)
data: 允许使用 data URI 格式的资源(例如,内嵌的图像)。
mediastream: 允许使用 mediastream: URI 格式的资源(例如,从摄像头或麦克风获取的媒体流)。
blob: 允许使用 blob: URI 格式的资源(例如,从 JavaScript 代码创建的二进制数据)。
filesystem: 允许使用 filesystem: URI 格式的资源(例如,从 HTML5 文件系统 API 创建的文件)。
https://example.com 允许从 https://example.com 域名加载资源。
*.example.com 允许从 example.com 及其所有子域名加载资源。

使用 nonce 和 hash 增强安全性

'unsafe-inline''unsafe-eval' 会降低 CSP 的安全性,应该尽量避免使用。如果必须使用内联脚本或 eval(),可以使用 noncehash 来更精确地控制允许执行的脚本。

  • Nonce: Nonce 是一个每次请求都会变化的随机字符串。为每个内联脚本添加 nonce 属性,并在 CSP 中使用 'nonce-{nonce-value}' 指令,可以只允许执行具有正确 nonce 值的脚本。

    例如:

    <script nonce="EyjJRAeu9T69s7j5">
      // 内联脚本
    </script>

    CSP 头:

    Content-Security-Policy: script-src 'self' 'nonce-EyjJRAeu9T69s7j5';

    在 WordPress 中,你需要动态生成 nonce 值,并在每个请求中将其添加到 CSP 头和内联脚本中。

    <?php
    function add_csp_header() {
      $nonce = wp_generate_password(24, false); // 生成一个随机 nonce
      header("Content-Security-Policy: script-src 'self' 'nonce-$nonce'");
      // 将 nonce 存储在全局变量中,以便在模板中使用
      global $csp_nonce;
      $csp_nonce = $nonce;
    }
    add_action('send_headers', 'add_csp_header');
    
    // 在模板中使用 nonce
    function get_csp_nonce() {
      global $csp_nonce;
      return $csp_nonce;
    }
    ?>
    
    <script nonce="<?php echo esc_attr(get_csp_nonce()); ?>">
      // 内联脚本
    </script>
  • Hash: Hash 是脚本内容的 SHA256、SHA384 或 SHA512 哈希值。在 CSP 中使用 'hash-{algorithm}-{base64-encoded-hash}' 指令,可以只允许执行具有指定哈希值的脚本。

    例如:

    <script>
      // 内联脚本
    </script>

    计算脚本内容的 SHA256 哈希值(例如,使用 openssl dgst -sha256 -binary your_script.js | openssl base64 命令)。

    CSP 头:

    Content-Security-Policy: script-src 'self' 'sha256-your-script-hash';

    与 nonce 相比,hash 的缺点是每次修改脚本内容都需要重新计算哈希值。

在 WordPress 中实施 CSP

在 WordPress 中,可以通过以下几种方式实施 CSP:

  1. 使用 HTTP 响应头: 这是最常见的方法。可以通过修改 WordPress 主题的 functions.php 文件或使用插件来添加 CSP 头。

    <?php
    function add_csp_header() {
      header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'");
    }
    add_action('send_headers', 'add_csp_header');
    ?>

    这个示例添加了一个基本的 CSP 头,只允许从当前域名加载脚本和样式表。你需要根据你的网站需求调整 CSP 指令。

  2. 使用 .htaccess 文件: 也可以在 .htaccess 文件中添加 CSP 头。

    <IfModule mod_headers.c>
      Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'"
    </IfModule>

    这种方法的优点是不需要修改 WordPress 代码,但需要服务器支持 mod_headers 模块。

  3. 使用 WordPress 插件: 有一些 WordPress 插件可以帮助你配置和管理 CSP。这些插件通常提供一个用户界面,可以更方便地设置 CSP 指令。 比如 Security Headers 插件。

CSP 报告

CSP 允许你配置一个报告 URI,浏览器会将 CSP 违反报告发送到该 URI。这可以帮助你监控 CSP 的效果,并发现潜在的安全问题。

可以使用 report-urireport-to 指令来配置报告 URI。

Content-Security-Policy: default-src 'self'; report-uri /csp-report-endpoint;

或者,使用 report-to 指令:

Content-Security-Policy: default-src 'self'; report-to csp-endpoint;

Report-To: {"group":"csp-endpoint","max_age":31536000,"endpoints":[{"url":"/csp-report-endpoint"}]}

你需要创建一个 /csp-report-endpoint 端点来接收和处理 CSP 报告。

<?php
// 处理 CSP 报告
if ($_SERVER['REQUEST_URI'] == '/csp-report-endpoint' && $_SERVER['REQUEST_METHOD'] == 'POST') {
  $report = json_decode(file_get_contents('php://input'), true);
  // 将报告记录到日志或数据库中
  error_log('CSP Violation: ' . json_encode($report));
  // 可以发送邮件通知管理员
  // mail('[email protected]', 'CSP Violation', json_encode($report));
  http_response_code(204); // 返回 204 No Content 状态码
  exit;
}
?>

部署 CSP 的步骤

  1. 评估你的网站: 首先,需要评估你的网站使用的所有资源,包括脚本、样式表、图像、字体等。确定这些资源的来源。
  2. 制定 CSP 策略: 根据你的网站需求,制定一个 CSP 策略。从一个宽松的策略开始,逐步收紧。
  3. 实施 CSP: 使用上述方法之一,将 CSP 策略添加到你的网站。
  4. 监控 CSP 报告: 配置 CSP 报告,并监控报告,以发现 CSP 违反。
  5. 调整 CSP 策略: 根据 CSP 报告,调整 CSP 策略,直到没有 CSP 违反为止。
  6. 强制执行 CSP: 将 CSP 策略从报告模式切换到强制执行模式。 (通过移除 Content-Security-Policy-Report-Only 头,并使用 Content-Security-Policy 头).

CSP 的最佳实践

  • 从宽松的策略开始: 不要一开始就使用一个非常严格的 CSP 策略,否则可能会导致网站无法正常工作。
  • 使用 CSP 报告: 监控 CSP 报告,以发现 CSP 违反。
  • 逐步收紧 CSP 策略: 根据 CSP 报告,逐步收紧 CSP 策略。
  • 使用 nonce 或 hash: 如果必须使用内联脚本或 eval(),可以使用 noncehash 来更精确地控制允许执行的脚本。
  • 保持 CSP 策略的更新: 随着网站的变化,需要定期更新 CSP 策略。
  • 测试 CSP 策略: 在生产环境中部署 CSP 策略之前,先在测试环境中进行测试。
  • 考虑使用 CSP 插件: 一些 WordPress 插件可以帮助你配置和管理 CSP。

常见问题和解决方法

  • CSP 阻止了网站的正常功能: 检查 CSP 报告,找出被阻止的资源,并调整 CSP 策略。
  • 无法使用内联脚本: 使用 noncehash 来允许特定的内联脚本。
  • 无法加载第三方资源: 将第三方资源的域名添加到 CSP 策略中。
  • CSP 报告没有收到: 检查报告 URI 是否正确,以及服务器是否正确处理 CSP 报告。
  • CSP 太复杂,难以理解: 从一个简单的 CSP 策略开始,逐步增加复杂度。使用 CSP 插件可以简化配置过程。

代码示例:完整的 functions.php CSP 实现

以下是一个更完整的 functions.php 文件示例,展示了如何动态生成 nonce,添加 CSP 头,并在模板中使用 nonce。 这个例子包括了script-srcstyle-src的nonce。

<?php

/**
 * 生成 CSP nonce
 */
function generate_csp_nonce() {
    return wp_generate_password(24, false);
}

/**
 * 添加 CSP 头部
 */
function add_csp_header() {
    $script_nonce = generate_csp_nonce();
    $style_nonce  = generate_csp_nonce();

    // 设置全局变量,以便在模板中使用
    global $csp_script_nonce, $csp_style_nonce;
    $csp_script_nonce = $script_nonce;
    $csp_style_nonce  = $style_nonce;

    $csp_header = "Content-Security-Policy: ";
    $csp_header .= "default-src 'self'; ";
    $csp_header .= "script-src 'self' 'nonce-$script_nonce'; ";
    $csp_header .= "style-src 'self' 'nonce-$style_nonce'; ";
    $csp_header .= "img-src 'self' data:; "; // 允许当前域名和data uri的图片
    $csp_header .= "font-src 'self'; "; // 允许当前域名字体
    $csp_header .= "connect-src 'self'; "; // 允许ajax请求
    $csp_header .= "media-src 'self'; "; // 允许当前域名媒体文件
    $csp_header .= "object-src 'none'; "; // 禁用object元素
    $csp_header .= "frame-ancestors 'self'; "; // 只允许当前域名嵌套
    $csp_header .= "form-action 'self'; "; // 只允许提交到当前域名
    $csp_header .= "block-all-mixed-content; "; // 升级所有不安全的请求
    $csp_header .= "upgrade-insecure-requests; "; // 升级所有http请求到https

    header($csp_header);
}
add_action('send_headers', 'add_csp_header');

/**
 * 获取 script nonce
 */
function get_csp_script_nonce() {
    global $csp_script_nonce;
    return $csp_script_nonce;
}

/**
 * 获取 style nonce
 */
function get_csp_style_nonce() {
    global $csp_style_nonce;
    return $csp_style_nonce;
}

/**
 * 在模板中添加 script nonce
 */
function inline_script( $script_content ) {
    $nonce = esc_attr( get_csp_script_nonce() );
    return '<script nonce="' . $nonce . '">' . $script_content . '</script>';
}

/**
 * 在模板中添加 style nonce
 */
function inline_style( $style_content ) {
    $nonce = esc_attr( get_csp_style_nonce() );
    return '<style nonce="' . $nonce . '">' . $style_content . '</style>';
}

// 如何使用
// echo inline_script("console.log('Hello from inline script!');");
// echo inline_style("body { background-color: #f0f0f0; }");

// 示例:如何安全地输出 WordPress 的设置到内联 JavaScript
function output_wordpress_settings() {
    $settings = array(
        'ajax_url' => admin_url('admin-ajax.php'),
        'theme_url' => get_stylesheet_directory_uri()
    );

    $json_settings = wp_json_encode( $settings );

    // 使用 wp_kses 过滤掉不安全的字符,虽然这里是配置,但是为了安全起见
    $safe_json_settings = wp_kses( $json_settings, array() );

    // 输出内联 JavaScript,并包含 CSP nonce
    echo inline_script( 'var wordpressSettings = ' . $safe_json_settings . ';' );
}
add_action( 'wp_head', 'output_wordpress_settings' );

?>

总结几句

Content Security Policy (CSP) 是一种重要的安全机制,它通过定义一个白名单来限制浏览器加载的资源,有效防御 XSS 攻击。 实施 CSP 需要仔细规划和测试,但它可以显著提高 WordPress 网站的安全性。 记住,安全是一个持续的过程,需要不断地评估和改进。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注