Laravel 的请求生命周期中,中间件(Middleware)构成了第一道防线。在防范 SQL 注入和 XSS 攻击时,很多人过度依赖 ORM 的自动转义或前端框架的自动转义,却忽略了在中间件层建立纵深防御体系。实际上,SQL 注入和 XSS 的防御层级完全不同,一个主要发生在数据进入数据库之前,另一个主要发生在数据输出到浏览器之前。理解这两者在中间件中的处理边界,才能真正堵住漏洞。

SQL 注入的防御核心不在中间件

先给一个明确的结论:SQL 注入的本质是将用户输入的数据当作 SQL 命令执行,而 Laravel 的 Eloquent ORM 和查询构建器默认使用 PDO 参数绑定,已经从根本上杜绝了 99% 的注入场景。只要你使用 where('column', $value) 而非 whereRaw("column = '$value'"),注入攻击就无从下手。中间件在这里的角色不是去拦截或过滤输入,而是做环境加固和异常请求阻断。比如,你可以在一个全局中间件中检测请求参数中是否包含典型的 SQL 注入探测字符串,如 UNION SELECT、1=1、-- 等模式,一旦命中直接返回 403 状态码并记录告警日志。这不是防御注入本身,而是降低攻击者的探测效率,同时为安全团队提供早期预警。

编写请求清洗中间件

在 Laravel 中创建一个中间件,对所有的 GET 和 POST 输入进行模式匹配,可以有效拦截自动化扫描工具。以下是一个基础实现:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SqlInjectionGuard
{
    protected $patterns = [
        '/\bUNION\b.*\bSELECT\b/i',
        '/\bSELECT\b.*\bFROM\b/i',
        '/\bINSERT\b\s+INTO\b/i',
        '/\bUPDATE\b\s+\w+\s+SET\b/i',
        '/\bDELETE\b\s+FROM\b/i',
        '/\bDROP\b\s+(TABLE|DATABASE)\b/i',
        '/\bOR\b\s+\'1\'=\'1/i',
        '/--\s*$/i',
        '/;\s*$/i',
    ];

    public function handle(Request $request, Closure $next)
    {
        $input = $request->all();
        
        array_walk_recursive($input, function ($value) {
            if (is_string($value)) {
                foreach ($this->patterns as $pattern) {
                    if (preg_match($pattern, $value)) {
                        abort(403, 'Malicious request detected.');
                    }
                }
            }
        });

        return $next($request);
    }
}

这个中间件注册为全局中间件后,每个请求都会经过检测。但必须注意,这种基于正则的检测存在误报风险。比如用户在富文本编辑器中正常讨论 SQL 语句时,输入内容可能包含 SELECT 关键字。因此,这个中间件更适合作为辅助防御层,而不是唯一的依赖。更精细的做法是结合路由分组,只对特定的 API 端点启用严格模式,对允许富文本输入的路由则放宽规则或跳过检测。

XSS 防御的中间件策略与输出编码

XSS 攻击的防御逻辑和 SQL 注入正好相反。SQL 注入要在数据入库前处理,XSS 则要在数据输出时处理。很多开发者犯的错误是在入库前对用户输入进行 HTML 实体编码,这会导致数据库中存储的是编码后的数据,当数据需要在非 HTML 场景(如 API 返回 JSON、移动端展示、邮件发送)中使用时,反而出现乱码或二次编码问题。正确的做法是:数据库存储原始用户输入,在输出到不同上下文时进行对应的编码处理。中间件在这里可以承担输出编码的职责,但需要区分响应类型。

响应中间件的上下文编码

创建一个响应中间件,根据响应的 Content-Type 决定是否对输出内容进行 HTML 编码。对于 text/html 类型的响应,中间件可以遍历响应数据,对用户提交的内容进行自动转义。但 Laravel 的 Blade 模板引擎已经默认使用了 {{ }} 双花括号语法进行 HTML 实体编码,只有使用 {!! !!} 才会输出原始 HTML。因此,中间件的职责更多是兜底:确保所有视图变量都经过编码,并检测是否有开发者误用了原始输出语法。以下是一个检测响应中是否包含未转义用户输入的中间件示例:

namespace App\Http\Middleware;

use Closure;

class XssOutputGuard
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        
        if ($this->isHtmlResponse($response)) {
            $content = $response->getContent();
            
            // 检测常见的XSS payload特征
            $dangerous = [
                '/]*>/i',
                '/on\w+\s*=\s*"[^"]*"/i',
                '/javascript\s*:/i',
                '/ $request->fullUrl(),
                        'pattern' => $pattern
                    ]);
                }
            }
        }
        
        return $response;
    }
    
    protected function isHtmlResponse($response): bool
    {
        $contentType = $response->headers->get('Content-Type');
        return $contentType && str_contains($contentType, 'text/html');
    }
}

这个中间件不修改响应内容,而是作为监控层存在。当检测到响应中可能包含未转义的脚本标签或事件处理属性时,记录日志供安全审计使用。这种非侵入式的检测既不会破坏现有功能,又能发现潜在的 XSS 漏洞。

内容安全策略(CSP)中间件的部署

真正有效的 XSS 防御不能只靠输入过滤和输出编码,还需要浏览器端的配合。Content-Security-Policy 响应头是现代浏览器提供的最后一道防线。在 Laravel 中间件中统一设置 CSP 头,可以限制脚本来源、禁止内联脚本执行、阻止不信任的资源加载。这是一个生产环境可用的 CSP 中间件实现:

namespace App\Http\Middleware;

use Closure;

class ContentSecurityPolicy
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        
        $csp = [
            "default-src 'self'",
            "script-src 'self' 'nonce-" . $this->generateNonce() . "'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self'",
            "frame-src 'none'",
            "object-src 'none'",
            "base-uri 'self'",
            "form-action 'self'",
        ];
        
        $response->headers->set(
            'Content-Security-Policy',
            implode('; ', $csp)
        );
        
        return $response;
    }
    
    protected function generateNonce(): string
    {
        return bin2hex(random_bytes(16));
    }
}

CSP 策略中,script-src 'self' 只允许加载同源的脚本文件,'nonce-' 机制允许带有特定随机数的内联脚本执行,而攻击者无法预测这个随机数,从而彻底封堵了反射型 XSS 和存储型 XSS 的脚本执行路径。需要注意的是,CSP 的 nonce 值需要传递给前端视图,在 Blade 模板中使用