服务器端防SQL注入不是靠某一项配置就能一劳永逸,而是要把“所有进入数据库的语句必须参数化”当成唯一原则,再配合输入边界控制、权限收敛、错误信息隔离和持续监控,形成多层防线。绝大多数注入成功的案例,根本原因不是WAF漏了,而是代码里存在拼接SQL的路径,或者数据库账号权限过大,导致攻击者一旦突破就能直接拖库。下面直接拆解服务器端最核心的配置方案。
1. 强制参数化查询,禁止任何形式的字符串拼接参数化查询是防SQL注入的根基。它的原理是把SQL结构和数据彻底分离,数据库驱动会把参数值当作纯数据处理,而不是SQL语法的一部分。无论攻击者输入什么内容,都不会改变SQL语句的逻辑结构。
在Java的JDBC PreparedStatement中,正确写法如下:
String customerName = request.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, customerName);
ResultSet results = pstmt.executeQuery();
错误做法是把用户输入直接拼进SQL字符串,例如:
String customerName = request.getParameter("customerName");
String query = "SELECT * FROM users WHERE name = '" + customerName + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
后者只要输入 "' OR '1'='1' --" 就能绕过条件。参数化查询不是可选优化,而是代码审查中的强制红线。对于ORM框架,也要检查是否使用了原生SQL拼接接口,例如JPA的 "createNativeQuery" 中直接拼接字符串,同样属于高危写法。
2. 存储过程不能替代参数化,调用方式必须同样严格很多团队误以为使用存储过程就天然防注入,实际上存储过程内部如果使用动态SQL拼接,照样会被注入。更常见的问题是,应用程序调用存储过程时用字符串拼接参数。
String sp = "call GetUser('" + userId + "')";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sp);
正确做法是使用CallableStatement,通过占位符传参:
CallableStatement cs = connection.prepareCall("{call GetUser(?)}");
cs.setString(1, userId);
ResultSet rs = cs.executeQuery();
存储过程内部的动态SQL必须使用参数化方式构建,例如在SQL Server中使用 "sp_executesql" 并传入参数,而不是直接拼接字符串执行。
3. 数据库账号最小权限原则,切断注入后的横向扩展能力即使参数化做得再完善,也要假设某个隐藏路径可能被绕过。因此数据库连接账号的权限必须严格收缩。应用连接数据库的账号只应拥有执行必要DML操作的权限,禁止拥有DDL权限、文件读写权限和系统存储过程执行权限。
以MySQL为例,应用账号配置建议如下:
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'%' IDENTIFIED BY 'strong_password'; REVOKE FILE, SUPER, PROCESS, RELOAD, SHUTDOWN ON *.* FROM 'app_user'@'%';
对于SQL Server,应用账号不应拥有 "sysadmin" 或 "db_owner" 角色,只授予 "db_datareader" 和 "db_datawriter",并禁止执行 "xp_cmdshell"、"sp_OACreate" 等危险扩展存储过程。这样即使攻击者找到注入点,也无法直接执行 "LOAD_FILE()"、"INTO OUTFILE" 或调用系统命令。
4. 输入验证与类型强制转换,在参数化之外再加一层边界控制参数化解决的是SQL语法注入问题,但业务逻辑漏洞依然存在。例如参数本身是数字类型,却传入了超长字符串,可能引发异常或逻辑绕过。服务器端必须对所有外部输入做白名单验证和类型强制转换。
对于ID类参数,强制转换为整数:
int id = Integer.parseInt(request.getParameter("id"));
对于枚举类参数,只允许白名单内的值:
String orderBy = request.getParameter("orderBy");
List<String> allowedColumns = Arrays.asList("id", "username", "create_time");
if (!allowedColumns.contains(orderBy)) {
throw new IllegalArgumentException("Invalid column");
}
对于字符串类参数,限制长度和字符集,例如只允许字母数字和下划线,拒绝单引号、分号、注释符等SQL特殊字符。虽然参数化已经让这些字符失去语法意义,但限制字符集可以进一步降低风险,同时防止二次注入和存储型XSS。
5. 错误信息脱敏,防止数据库结构泄露服务器端必须配置统一的异常处理,禁止将数据库原始错误信息返回给客户端。详细的SQL错误信息会暴露表名、字段名、数据库类型和部分查询逻辑,为攻击者提供精确的注入依据。
在Java Web应用中,web.xml或全局异常处理器中配置:
<error-page>
<exception-type>java.sql.SQLException</exception-type>
<location>/error.jsp</location>
</error-page>
在Spring Boot中,可以配置 "application.properties":
server.error.include-message=never server.error.include-stacktrace=never
客户端只返回统一的“系统繁忙,请稍后重试”或类似提示,详细错误写入服务器日志,供内部排查。
6. 数据库自身安全配置加固数据库层面的配置同样关键。首先要禁用不必要的数据库功能和组件。MySQL应禁用 "LOAD DATA LOCAL INFILE",在配置文件中设置:
[mysqld] local-infile=0
SQL Server应禁用 "xp_cmdshell":
EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;
其次,限制数据库监听地址,只允许应用服务器IP访问,禁止公网直接暴露数据库端口。对于MySQL,在 "my.cnf" 中设置:
bind-address = 10.0.0.5
同时启用数据库审计日志,记录所有DDL和敏感操作,便于事后追溯。
7. 使用预编译语句缓存,兼顾性能与安全很多开发人员担心参数化查询会影响性能,实际上现代数据库驱动和数据库本身对预编译语句有缓存机制。相同结构的SQL语句只需要编译一次,后续执行直接复用执行计划。对于高频查询,参数化查询的性能往往优于拼接SQL,因为拼接SQL每次都会重新解析和编译。
在连接池配置中,应启用预编译缓存。以HikariCP为例:
spring.datasource.hikari.data-source-properties.cachePrepStmts=true spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250 spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048
这样既能保证安全,又能获得稳定的执行效率。
8. 动态表名、列名和排序字段的处理方案参数化查询无法直接绑定表名、列名和排序方向这类SQL结构标识符。很多业务场景需要动态排序或动态表名,这时必须使用白名单映射,而不是直接拼接用户输入。
private static final Map<String, String> COLUMN_MAP = new HashMap<>();
static {
COLUMN_MAP.put("id", "id");
COLUMN_MAP.put("username", "username");
COLUMN_MAP.put("createTime", "create_time");
}
public List<User> getUsersByOrder(String orderBy) {
String column = COLUMN_MAP.get(orderBy);
if (column == null) {
throw new IllegalArgumentException("Invalid column");
}
String sql = "SELECT * FROM users ORDER BY " + column;
return jdbcTemplate.query(sql, new UserRowMapper());
}
这里虽然使用了字符串拼接,但拼接的内容完全来自服务器端定义的白名单,用户输入无法直接影响SQL结构,因此是安全的。
9. 多层防御:WAF与运行时应用自保护在应用层之外,部署WAF可以拦截明显的注入攻击载荷。但WAF不能替代代码修复,因为编码绕过、分块传输、双URL编码等技术可以绕过大多数WAF规则。更有效的做法是在应用内部引入运行时检测组件,例如Java Agent方式的RASP,在SQL执行前检查语句是否包含未参数化的用户输入。
WAF的配置重点应放在异常请求频率限制、SQL关键字组合检测和请求体深度解析上,但核心防线仍然是代码层的参数化。
10. 持续检测与回归测试防注入配置必须纳入CI/CD流程。每次代码提交都应运行自动化安全扫描,检查是否存在SQL拼接模式。可以在单元测试中直接注入恶意载荷,验证查询结果是否符合预期。
@Test
public void testSqlInjectionAttempt() {
String maliciousInput = "' OR '1'='1";
List<User> users = userService.findByName(maliciousInput);
assertTrue(users.isEmpty());
}
同时定期使用SQLMap等工具对测试环境进行自动化注入测试,验证多层防御的有效性。生产环境应持续监控数据库慢查询日志和异常错误日志,发现异常查询模式及时告警。
服务器端防SQL注入的核心配置方案可以概括为:参数化查询为唯一数据通道,白名单验证动态结构,最小权限限制爆炸半径,错误脱敏切断情报泄露,数据库加固关闭危险功能,最后用自动化测试和监控形成闭环。任何单一措施都有被绕过的可能,但多层叠加后,攻击者的成本会急剧上升,绝大多数自动化注入工具将无法奏效。
