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 |
定义允许使用 XMLHttpRequest 、Fetch 、WebSocket 等 API 连接的来源。 |
media-src |
定义允许加载音频和视频文件的来源。 |
object-src |
定义允许加载 <object> 、<embed> 和 <applet> 元素的来源。(不建议使用这些元素,因为它们存在安全风险。) |
frame-src |
定义允许加载 <frame> 和 <iframe> 元素的来源。 |
child-src |
frame-src 的别名,用于定义允许加载 Web Workers 和嵌套浏览上下文的来源。 (在某些浏览器中,frame-src 和 child-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()
,可以使用 nonce
或 hash
来更精确地控制允许执行的脚本。
-
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:
-
使用 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 指令。
-
使用
.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
模块。 -
使用 WordPress 插件: 有一些 WordPress 插件可以帮助你配置和管理 CSP。这些插件通常提供一个用户界面,可以更方便地设置 CSP 指令。 比如
Security Headers
插件。
CSP 报告
CSP 允许你配置一个报告 URI,浏览器会将 CSP 违反报告发送到该 URI。这可以帮助你监控 CSP 的效果,并发现潜在的安全问题。
可以使用 report-uri
或 report-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 的步骤
- 评估你的网站: 首先,需要评估你的网站使用的所有资源,包括脚本、样式表、图像、字体等。确定这些资源的来源。
- 制定 CSP 策略: 根据你的网站需求,制定一个 CSP 策略。从一个宽松的策略开始,逐步收紧。
- 实施 CSP: 使用上述方法之一,将 CSP 策略添加到你的网站。
- 监控 CSP 报告: 配置 CSP 报告,并监控报告,以发现 CSP 违反。
- 调整 CSP 策略: 根据 CSP 报告,调整 CSP 策略,直到没有 CSP 违反为止。
- 强制执行 CSP: 将 CSP 策略从报告模式切换到强制执行模式。 (通过移除
Content-Security-Policy-Report-Only
头,并使用Content-Security-Policy
头).
CSP 的最佳实践
- 从宽松的策略开始: 不要一开始就使用一个非常严格的 CSP 策略,否则可能会导致网站无法正常工作。
- 使用 CSP 报告: 监控 CSP 报告,以发现 CSP 违反。
- 逐步收紧 CSP 策略: 根据 CSP 报告,逐步收紧 CSP 策略。
- 使用 nonce 或 hash: 如果必须使用内联脚本或
eval()
,可以使用nonce
或hash
来更精确地控制允许执行的脚本。 - 保持 CSP 策略的更新: 随着网站的变化,需要定期更新 CSP 策略。
- 测试 CSP 策略: 在生产环境中部署 CSP 策略之前,先在测试环境中进行测试。
- 考虑使用 CSP 插件: 一些 WordPress 插件可以帮助你配置和管理 CSP。
常见问题和解决方法
- CSP 阻止了网站的正常功能: 检查 CSP 报告,找出被阻止的资源,并调整 CSP 策略。
- 无法使用内联脚本: 使用
nonce
或hash
来允许特定的内联脚本。 - 无法加载第三方资源: 将第三方资源的域名添加到 CSP 策略中。
- CSP 报告没有收到: 检查报告 URI 是否正确,以及服务器是否正确处理 CSP 报告。
- CSP 太复杂,难以理解: 从一个简单的 CSP 策略开始,逐步增加复杂度。使用 CSP 插件可以简化配置过程。
代码示例:完整的 functions.php CSP 实现
以下是一个更完整的 functions.php
文件示例,展示了如何动态生成 nonce,添加 CSP 头,并在模板中使用 nonce。 这个例子包括了script-src
和style-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 网站的安全性。 记住,安全是一个持续的过程,需要不断地评估和改进。