网站被挂马、数据被拖库、后台被篡改,绝大多数攻击入口都集中在SQL注入。很多人以为装个WAF或者用参数化查询就万事大吉,实际上真正有效的防护必须从Web服务器、应用代码、数据库账户、运行权限四个层面同时收紧。单一环节的防护在攻击链面前基本等于没有防护。

SQL注入的本质是用户输入的数据被数据库当作SQL语句的一部分执行了。攻击者通过构造特殊的参数,让原本的查询逻辑发生改变,从而绕过登录验证、读取敏感数据、写入Webshell甚至直接执行系统命令。要阻断这条路,必须让数据永远只作为数据处理,而不是作为代码执行。

一、Web服务器层的访问过滤

在请求到达应用代码之前,Web服务器可以先做一轮粗筛。Nginx和Apache都可以通过规则模块拦截明显带有SQL特征参数的请求。以Nginx为例,可以在server块中加入对常见SQL关键字的过滤,拦截URI和查询字符串中出现union select、sleep(、benchmark(、information_schema等特征的请求。

location / {
    if ($query_string ~* "union.*select|select.*from|sleep\(|benchmark\(|information_schema|load_file|outfile") {
        return 403;
    }
    if ($request_uri ~* "union.*select|select.*from|sleep\(|benchmark\(|information_schema|load_file|outfile") {
        return 403;
    }
}

这种过滤不能替代应用层的防护,但可以挡住大量自动化扫描工具和脚本小子的试探性攻击。需要注意的是规则不能写得过于宽泛,否则可能误伤正常业务参数,比如文章内容里包含select这个词就会触发拦截。实践中建议只针对GET参数做严格过滤,POST数据交给应用层处理。

二、应用代码层的参数化查询

参数化查询是防SQL注入最核心的手段。它的原理是把SQL语句结构和数据分开,数据库驱动会将参数值当作纯数据处理,无论参数里写什么都不会被解析成SQL语法。以PHP的PDO为例,正确的写法是:

$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND status = 1');
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->execute();

错误写法是把变量直接拼接进SQL字符串,即使外面套了过滤函数也依然危险。mysqli同样支持预处理语句,但要注意必须使用bind_param绑定参数,而不是用real_escape_string手动转义后再拼接。转义函数受字符集、版本和上下文影响,历史上多次出现过绕过案例,不能作为主要防线。

对于无法使用预处理语句的场景,比如动态表名、动态排序字段、IN子句的可变数量参数,必须使用白名单校验。排序字段只允许在id、create_time、update_time等固定列表中选择,表名只允许在预设的表名数组中匹配,任何不在白名单内的输入直接拒绝。

三、数据库账户权限最小化

很多网站被注入后损失惨重,根本原因是应用连接数据库使用的账户权限过大。一个普通的Web应用账户根本不需要FILE、DROP、ALTER、CREATE、SHUTDOWN这些权限,甚至不需要对全部表有读写权限。应该为不同的业务模块创建不同的数据库账户,每个账户只授予必要的表级权限。

以MySQL为例,创建一个仅用于Web查询和写入的账户,只开放指定数据库的SELECT、INSERT、UPDATE、DELETE权限:

CREATE USER 'web_app'@'localhost' IDENTIFIED BY '强密码';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'web_app'@'localhost';
FLUSH PRIVILEGES;

即使攻击者成功注入,拿到了数据库账户,他能做的也只是对mydb库中的表做增删改查,无法读写服务器文件、无法执行系统命令、无法跨库读取其他数据库。配合应用层对敏感表的分级授权,比如用户表只允许SELECT和UPDATE不允许DELETE,订单表只允许INSERT和SELECT,可以把注入后的损失控制在最小范围。

四、运行环境与文件系统隔离

SQL注入经常被用来写Webshell,MySQL的INTO OUTFILE和LOAD_FILE函数在权限允许时可以读写服务器文件。即使数据库账户没有FILE权限,攻击者也可能通过注入获取后台管理员密码,登录后台后利用上传功能传马。因此Web服务器运行账户的权限同样需要严格限制。

PHP-FPM和Nginx/Apache的运行用户应该设置为独立的低权限账户,该账户只对网站根目录有读取权限,对上传目录有写入权限,对其他系统目录没有任何访问权限。同时关闭PHP的危险函数,在php.ini中设置:

disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

这些函数在正常业务中很少用到,但被攻击者拿到代码执行权限后却是进一步提权的关键工具。关闭它们可以显著提高攻击成本。

五、日志监控与入侵检测

防护做得再好,也需要监控来兜底。Web服务器访问日志和数据库错误日志是发现SQL注入尝试的重要数据源。Nginx日志中如果出现大量包含union select、sleep(、information_schema的请求,说明有人在扫描注入点。MySQL的general_log和slow_query_log可以记录实际执行的SQL语句,帮助确认是否有恶意语句被执行。

对于生产环境,建议部署开源的日志分析工具,比如使用Fail2ban监控Nginx日志,对短时间内触发大量403规则的IP自动封禁。同时定期人工检查数据库中的异常数据,比如管理员表是否多出未知账户、文章内容中是否被插入script标签、数据库中是否出现可疑的存储过程或触发器。

六、一体化搭建的具体顺序

新站搭建时,建议按照以下顺序完成安全配置:先创建最小权限的数据库账户,再编写使用参数化查询的应用代码,然后配置Web服务器的请求过滤规则,接着收紧运行账户和PHP函数权限,最后开启日志监控。这个顺序能保证每一步都在前一步的基础上叠加防护,避免出现先上线后补安全的空窗期。

对于已上线的老站,可以先从数据库账户权限入手,因为这一步对现有代码影响最小。然后逐步替换代码中的字符串拼接查询为预处理语句,优先处理登录、搜索、列表页等直接接收用户输入的接口。最后再调整Web服务器规则和运行环境,避免一开始就上过滤规则导致正常业务被误伤。

七、常见误区与补充说明

很多人认为HTTPS能防SQL注入,这是完全错误的理解。HTTPS只加密传输过程,防止中间人窃听,对注入攻击没有任何防护作用。还有人认为使用存储过程可以防注入,实际上如果存储过程内部使用了动态拼接SQL,同样存在注入风险。ORM框架也不能完全杜绝注入,如果代码中使用了原生SQL拼接或者不当的查询构造方式,ORM一样会被注入。

SQL注入防护不是某一个技术点的单点防御,而是从请求入口到数据库执行的全链路收紧。任何一个环节的疏忽都可能导致整体防线失效。安全配置需要根据业务实际情况调整,不能照搬网上的规则模板,但参数化查询和最小权限这两个原则是通用的,必须严格执行。