ThinkPHP项目CSP内容安全策略

wen PHP项目 3

本文目录导读:

ThinkPHP项目CSP内容安全策略

  1. CSP基础配置
  2. 动态CSP配置
  3. 视图中的CSP集成
  4. 高级CSP配置
  5. 测试和调试
  6. 最佳实践建议

我来详细介绍ThinkPHP项目中实施CSP(内容安全策略)的方法:

CSP基础配置

1 通过中间件添加CSP头

app/middleware.php中注册CSP中间件:

<?php
// app/middleware.php
return [
    // 全局CSP中间件
    \app\middleware\CspMiddleware::class,
];

2 创建CSP中间件

<?php
// app/middleware/CspMiddleware.php
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
class CspMiddleware
{
    /**
     * CSP策略配置
     */
    protected $cspConfig = [
        'default-src' => "'self'",
        'script-src' => "'self' 'unsafe-inline' 'unsafe-eval'",
        'style-src' => "'self' 'unsafe-inline' https://cdn.example.com",
        'img-src' => "'self' data: https://*.example.com",
        'font-src' => "'self' https://fonts.googleapis.com https://fonts.gstatic.com",
        'connect-src' => "'self'",
        'frame-src' => "'self'",
        'object-src' => "'none'",
        'base-uri' => "'self'",
        'form-action' => "'self'",
        'frame-ancestors' => "'self'",
        'upgrade-insecure-requests' => '',
    ];
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        // 构建CSP头部
        $cspHeader = $this->buildCspHeader();
        // 添加CSP响应头
        $response->header([
            'Content-Security-Policy' => $cspHeader,
            'X-Content-Security-Policy' => $cspHeader, // 旧浏览器兼容
            'X-WebKit-CSP' => $cspHeader, // WebKit浏览器兼容
        ]);
        return $response;
    }
    /**
     * 构建CSP头部字符串
     */
    protected function buildCspHeader(): string
    {
        $directives = [];
        foreach ($this->cspConfig as $key => $value) {
            if ($value !== '') {
                $directives[] = "{$key} {$value}";
            } else {
                $directives[] = $key;
            }
        }
        return implode('; ', $directives);
    }
}

动态CSP配置

1 配置文件方式

<?php
// config/csp.php
return [
    // 开发环境配置
    'development' => [
        'default-src' => "'self'",
        'script-src' => "'self' 'unsafe-inline' 'unsafe-eval'",
        'style-src' => "'self' 'unsafe-inline'",
        'img-src' => "'self' data:",
        'connect-src' => "'self' ws://localhost:*",
    ],
    // 生产环境配置
    'production' => [
        'default-src' => "'self'",
        'script-src' => "'self'",
        'style-src' => "'self'",
        'img-src' => "'self' data:",
        'connect-src' => "'self'",
        'font-src' => "'self' https://fonts.googleapis.com https://fonts.gstatic.com",
        'object-src' => "'none'",
        'base-uri' => "'self'",
        'form-action' => "'self'",
        'frame-ancestors' => "'self'",
        'upgrade-insecure-requests' => '',
    ],
];

2 改进中间件支持配置

<?php
// app/middleware/CspMiddleware.php
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
use think\facade\Config;
class CspMiddleware
{
    protected $cspConfig = [];
    protected $noncePool = [];
    public function __construct()
    {
        // 从配置文件加载CSP设置
        $environment = app()->isDebug() ? 'development' : 'production';
        $this->cspConfig = Config::get('csp.' . $environment);
        // 生成nonce值
        $this->generateNonces();
    }
    /**
     * 生成CSP nonce
     */
    protected function generateNonces(): void
    {
        $this->noncePool = [
            'script' => base64_encode(random_bytes(16)),
            'style' => base64_encode(random_bytes(16)),
        ];
    }
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        // 应用CSP头部
        $this->applyCspHeaders($response);
        // 注入nonce到响应
        if ($response instanceof Response && !$request->isAjax()) {
            $this->injectNonces($response);
        }
        return $response;
    }
    /**
     * 应用CSP头
     */
    protected function applyCspHeaders(Response $response): void
    {
        $cspHeader = $this->buildCspHeader();
        $response->header([
            'Content-Security-Policy' => $cspHeader,
            'X-Content-Security-Policy' => $cspHeader,
            'X-WebKit-CSP' => $cspHeader,
        ]);
    }
    /**
     * 构建CSP头
     */
    protected function buildCspHeader(): string
    {
        $directives = [];
        foreach ($this->cspConfig as $key => $value) {
            // 替换nonce占位符
            if (strpos($value, '{nonce}') !== false) {
                $value = str_replace('{nonce}', "'nonce-{$this->noncePool[$key] ?? ''}'", $value);
            }
            $directives[] = $value === '' ? $key : "{$key} {$value}";
        }
        return implode('; ', $directives);
    }
    /**
     * 注入nonce到HTML
     */
    protected function injectNonces(Response $response): void
    {
        $content = $response->getContent();
        // 替换script标签
        $content = preg_replace(
            '/<script(?!.*nonce)/i',
            '<script nonce="' . $this->noncePool['script'] . '"',
            $content
        );
        // 替换style标签
        $content = preg_replace(
            '/<style(?!.*nonce)/i',
            '<style nonce="' . $this->noncePool['style'] . '"',
            $content
        );
        $response->content($content);
    }
}

视图中的CSP集成

1 在模板中使用nonce

<!-- application/index/view/index/index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">ThinkPHP CSP</title>
    <!-- 使用nonce的样式 -->
    <style nonce="<?= $csp['style'] ?>">
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 5px;
        }
    </style>
    <!-- 内联脚本使用nonce -->
    <script nonce="<?= $csp['script'] ?>">
        document.addEventListener('DOMContentLoaded', function() {
            console.log('页面加载完成');
        });
    </script>
</head>
<body>
    <div class="container">
        <h1>内容安全策略演示</h1>
        <!-- 外部脚本 -->
        <script src="/static/js/app.js" nonce="<?= $csp['script'] ?>"></script>
        <!-- 内联事件处理器可能需要额外处理 -->
        <button onclick="handleClick()" data-nonce="<?= $csp['script'] ?>">点击我</button>
    </div>
</body>
</html>

2 控制器中传递nonce

<?php
// app/controller/Index.php
namespace app\controller;
use think\Request;
class Index
{
    public function index(Request $request)
    {
        // 获取中间件生成的nonce
        $csp = [
            'script' => $request->middleware('nonce_script'),
            'style' => $request->middleware('nonce_style'),
        ];
        return view('index/index', ['csp' => $csp]);
    }
}

高级CSP配置

1 CSP报告功能

<?php
// app/middleware/CspReportMiddleware.php
namespace app\middleware;
use Closure;
use think\Request;
class CspReportMiddleware
{
    protected $reportUri = '/api/csp/report';
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        // 添加报告URI
        $response->header('Content-Security-Policy-Report-Only', 
            "default-src 'self'; report-uri {$this->reportUri}");
        return $response;
    }
}
// app/controller/api/CspReport.php
namespace app\controller\api;
use think\Request;
use think\facade\Log;
class CspReport
{
    public function report(Request $request)
    {
        // 获取CSP违规报告
        $data = $request->post();
        // 记录违规信息
        Log::error('CSP Violation:', $data);
        // 可以在数据库中记录
        // 可以发送邮件通知
        return json(['status' => 'success']);
    }
}

2 按路由应用不同策略

<?php
// route/app.php
use think\facade\Route;
// 管理路由使用更严格的CSP
Route::group('admin', function () {
    Route::get('dashboard', 'admin/Dashboard/index');
    Route::get('settings', 'admin/Settings/index');
})->middleware(\app\middleware\StrictCspMiddleware::class);
// API路由使用CORS配置
Route::group('api', function () {
    // API路由
})->middleware(\app\middleware\ApiCspMiddleware::class);
// 前端路由使用宽松CSP
Route::group('frontend', function () {
    // 前端路由
})->middleware(\app\middleware\FrontendCspMiddleware::class);

3 完整的CSP配置类

<?php
// app/library/Csp/CspFactory.php
namespace app\library\Csp;
class CspFactory
{
    /**
     * 创建严格CSP
     */
    public static function strict(): array
    {
        return [
            'default-src' => "'none'",
            'script-src' => "'self'",
            'style-src' => "'self'",
            'img-src' => "'self' data:",
            'connect-src' => "'self'",
            'font-src' => "'self'",
            'object-src' => "'none'",
            'frame-ancestors' => "'none'",
            'base-uri' => "'self'",
            'form-action' => "'self'",
        ];
    }
    /**
     * 创建一般CSP
     */
    public static function standard(): array
    {
        return [
            'default-src' => "'self'",
            'script-src' => "'self' 'unsafe-inline'",
            'style-src' => "'self' 'unsafe-inline'",
            'img-src' => "'self' data: https:",
            'font-src' => "'self' https:",
            'connect-src' => "'self'",
            'object-src' => "'none'",
            'frame-ancestors' => "'self'",
            'base-uri' => "'self'",
            'form-action' => "'self'",
        ];
    }
    /**
     * 创建宽松CSP
     */
    public static function relaxed(): array
    {
        return [
            'default-src' => "'self' *",
            'script-src' => "'self' 'unsafe-inline' 'unsafe-eval' *",
            'style-src' => "'self' 'unsafe-inline' *",
            'img-src' => "'self' data: blob: *",
            'font-src' => "'self' data: *",
            'connect-src' => "'self' *",
            'object-src' => "'none'",
            'frame-ancestors' => "*",
        ];
    }
}

4 动态CSP适配器

<?php
// app/middleware/AdaptiveCspMiddleware.php
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
use app\library\Csp\CspFactory;
class AdaptiveCspMiddleware
{
    protected $cspConfig = [];
    public function handle(Request $request, Closure $next)
    {
        // 根据用户代理和路由动态选择CSP策略
        $this->cspConfig = $this->selectCspPolicy($request);
        $response = $next($request);
        // 应用CSP头
        $cspHeader = $this->buildCspHeader();
        $response->header('Content-Security-Policy', $cspHeader);
        return $response;
    }
    /**
     * 选择CSP策略
     */
    protected function selectCspPolicy(Request $request): array
    {
        // 判断是否为后台管理页面
        if (strpos($request->pathinfo(), 'admin') === 0) {
            return CspFactory::strict();
        }
        // 判断是否为API请求
        if (strpos($request->pathinfo(), 'api') === 0) {
            return CspFactory::api();
        }
        // 判断是否为移动端
        $userAgent = $request->header('user-agent');
        if (preg_match('/Mobile|Android|iPhone/i', $userAgent)) {
            return CspFactory::mobile();
        }
        // 默认使用标准策略
        return CspFactory::standard();
    }
    /**
     * 构建CSP头
     */
    protected function buildCspHeader(): string
    {
        $directives = [];
        foreach ($this->cspConfig as $directive => $value) {
            $directives[] = "{$directive} {$value}";
        }
        return implode('; ', $directives);
    }
}

测试和调试

1 CSP测试页面

<?php
// app/controller/CspTest.php
namespace app\controller;
use think\Request;
class CspTest
{
    public function index()
    {
        $scripts = [
            'inline' => '<script>console.log("测试内联脚本");</script>',
            'external' => '<script src="/static/js/test.js"></script>',
            'external_cdn' => '<script src="https://cdn.example.com/js/lib.js"></script>'
        ];
        return view('csp/test', ['scripts' => $scripts]);
    }
    public function report(Request $request)
    {
        $report = json_decode($request->getContent(), true);
        // 日志记录
        trace($report, 'CSP Violation');
        return json(['status' => 'ok']);
    }
}

2 CSP监控配置

<?php
// config/csp_monitor.php
return [
    'enabled' => true,
    'log_level' => 'warning',
    'report_threshold' => 10, // 每分钟报告次数阈值
    'report_endpoint' => '/api/csp/report',
    'allowed_scripts' => [
        'https://cdn.example.com',
        'https://statistics.example.com',
    ],
    'allowed_styles' => [
        'https://fonts.googleapis.com',
    ],
    'allowed_images' => [
        'https://*.example.com',
    ],
];

3 CSP安全检查工具

<?php
// app/library/Csp/CspValidator.php
namespace app\library\Csp;
class CspValidator
{
    /**
     * 验证CSP策略
     */
    public static function validate(array $policy): array
    {
        $errors = [];
        $warnings = [];
        // 检查是否配置了默认策略
        if (!isset($policy['default-src'])) {
            $errors[] = '缺少 default-src 指令';
        }
        // 检查是否允许了危险的指令
        foreach (['script-src', 'style-src'] as $directive) {
            if (isset($policy[$directive])) {
                if (strpos($policy[$directive], "'unsafe-inline'") !== false) {
                    $warnings[] = "{$directive} 包含 unsafe-inline";
                }
                if (strpos($policy[$directive], "'unsafe-eval'") !== false) {
                    $warnings[] = "{$directive} 包含 unsafe-eval";
                }
            }
        }
        // 检查是否配置了报告URI
        if (!isset($policy['report-uri']) && !isset($policy['report-to'])) {
            $warnings[] = '未配置报告URI,无法监控违规行为';
        }
        return [
            'errors' => $errors,
            'warnings' => $warnings,
            'valid' => empty($errors),
        ];
    }
}

最佳实践建议

1 部署前检查清单

<?php
// app/command/CheckCsp.php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use app\library\Csp\CspFactory;
use app\library\Csp\CspValidator;
class CheckCsp extends Command
{
    protected function configure()
    {
        $this->setName('check:csp')
             ->setDescription('检查CSP配置');
    }
    protected function execute(Input $input, Output $output)
    {
        $output->writeln('开始检查CSP配置...');
        // 检查所有环境的CSP配置
        foreach (['development', 'production', 'test'] as $env) {
            $output->writeln("检查{$env}环境:");
            $config = Config::get('csp.' . $env);
            $result = CspValidator::validate($config);
            if (!$result['valid']) {
                foreach ($result['errors'] as $error) {
                    $output->writeln("  <error>[错误] {$error}</error>");
                }
            }
            foreach ($result['warnings'] as $warning) {
                $output->writeln("  <comment>[警告] {$warning}</comment>");
            }
            if (empty($result['errors']) && empty($result['warnings'])) {
                $output->writeln("  <info>[OK] 配置良好</info>");
            }
        }
        // 检查静态资源
        $this->checkStaticResources($output);
        $output->writeln('CSP检查完成');
    }
    protected function checkStaticResources(Output $output)
    {
        $output->writeln('检查静态资源...');
        // 检查JavaScript文件
        $jsFiles = glob(root_path('public/static/js/*.js'));
        foreach ($jsFiles as $file) {
            $content = file_get_contents($file);
            if (preg_match('/\beval\s*\(/i', $content)) {
                $output->writeln("  <comment>[警告] 文件包含eval: {$file}</comment>");
            }
        }
    }
}

2 安全建议

// 1. 不在CSP中添加过多的域名白名单
// 2. 使用nonce而不是allowlist管理内联脚本
// 3. 定期检查CSP报告
// 4. 使用Report-Only模式测试
// 5. 为不同环境设置不同的CSP策略
// 6. 考虑使用SRI(子资源完整性)

通过以上配置,你可以为ThinkPHP项目建立完善的内容安全策略,有效防范XSS攻击和数据注入攻击,记得根据实际需求调整配置,并在部署前充分测试。

抱歉,评论功能暂时关闭!