避免 SQL 注入最可靠的手段只有一条:永远不要把用户输入直接拼进 SQL 字符串。核心做法是参数化查询(预编译语句)——先向数据库发送固定结构的 SQL 模板,再把用户数据作为独立参数传递,数据库始终区分”代码”与”数据”,攻击者即使输入 tom' OR '1'='1 也只会被当作一个普通字符串去匹配。据 OWASP 注入防护速查表,参数化查询、正确构建的存储过程、允许列表输入验证是三大主要防御;转义输入仅作为最后的退路。SQL 注入常年位居 OWASP Top 10,根源就是”用字符串拼接 + 用户输入构造动态查询”,任何语言、任何数据库都可能中招。
一、SQL 注入在怎么发生
漏洞来自”把用户参数直接追加到查询字符串”。OWASP 给出的典型反面案例:
// 危险:customerName 被直接拼进 SQL
String query = "SELECT account_balance FROM user_data WHERE user_name = "
+ request.getParameter("customerName");
Statement statement = connection.createStatement();
ResultSet results = statement.executeQuery(query);
若攻击者传入 tom' OR '1'='1,拼出的 SQL 变成 ... WHERE user_name = 'tom' OR '1'='1',条件恒真,直接拖出全表。这正是拼接动态查询的代价——数据库无法分辨哪部分是代码、哪部分是数据。
二、主要防御一:参数化查询(首选)
参数化查询(Prepared Statement)强制开发者先定义全部 SQL 代码,再单独传入每个参数。据 OWASP 与 decidestack 的安全指南,只要数据库查询用这种风格,无论输入什么,数据库都区分代码与数据,攻击者无法改变查询意图。
// 安全:占位符 ? 与参数分离
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, custname); // 参数按字符串原样绑定,不解析为 SQL
ResultSet results = pstmt.executeQuery();
Python 等价写法:
# 安全:用参数元组,绝不做字符串格式化
cursor.execute(
"SELECT account_balance FROM user_data WHERE user_name = %s",
(custname,)
)
OWASP 强调,几乎所有语言(Java、.NET、PHP、Ruby、Python、Cold Fusion)与 ORM(如 Hibernate 的命名参数 :productid、Prisma 默认参数化)都支持参数化接口。decidestack 给出黄金法则:”Never Trust User Input”——即便认为输入安全,也要当作敌对数据处理。
三、主要防御二:正确构建的存储过程
存储过程的 SQL 代码在数据库中预定义、由应用调用,若内部不使用不安全的动态 SQL 拼接,其防注入效果与参数化查询相同。OWASP 指出二者本质等价,组织按习惯二选一即可。
// 安全:存储过程也用参数绑定,而非内部拼接
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
注意陷阱:存储过程并非天然安全。若过程内部用 EXEC/sp_execute 拼动态 SQL,仍会注入。OWASP 提醒审计人员应排查这类动态拼接;MS SQL Server 上若应用为跑存储过程而被授 db_owner,一旦被攻破危害更大,应遵循最小权限。
四、主要防御三:允许列表输入验证
绑定变量无法用于表名、列名、排序方向(ASC/DESC)等位置——这些地方必须靠输入验证或查询重设计。OWASP 的建议是:理想情况下表名/列名来自代码而非用户参数;若必须用用户值,把它映射到合法预期值(白名单)。
// 安全:把用户选择的排序方向映射成固定值,而不是原样拼接
String orderSql = sortOrder ? "ASC" : "DESC";
String sql = "SELECT ... ORDER BY Salary " + orderSql;
通用原则:任何用户输入在进查询前,若能转成非字符串类型(数字、布尔、枚举、日期),先转换再使用。OWASP 与 decidestack 都建议把输入验证作为二级防御,叠加在参数化之上。
五、主要防御四:转义(仅最后退路)
OWASP 明确标注”强烈不推荐”把转义所有用户输入当作主要防御。它依赖具体数据库的转义函数、实现脆弱,无法保证在所有情况下防住注入,通常只用于老旧代码改造。新项目应直接用参数化查询、存储过程或 ORM 重写。
六、额外纵深防御
除四大主要防御外,OWASP 与 decidestack 建议叠加下列措施形成纵深防御:
- 最小权限原则:应用数据库账户只授必要权限,只读账户不给
DROP/GRANT;即便被注入,破坏范围也受限。 - ORM 与查询构造器:现代 ORM 默认参数化,但慎用其
raw/原生 SQL 方法,避免内部拼接。 - Web 应用防火墙(WAF):在边界过滤已知注入特征,拦自动化攻击,属外围兜底而非替代。
- 保持驱动与框架更新:及时打安全补丁。
七、落地执行步骤
按 OWASP 与 decidestack 的实践顺序:
- 禁用字符串拼接:全局排查
SELECT ... + 变量写法,全部改为参数化。 - 选参数化接口:Java 用
PreparedStatement、.NET 用Parameters.Add、Python 用参数元组、ORM 用命名参数。 - 非绑定位置(表名/列名/排序)用白名单映射,杜绝用户值直接进 SQL。
- 加输入验证:类型、长度、格式校验,作为二级防线。
- 收最小权限:应用账户降权,去掉 DBA/管理员权限。
- 加 WAF 与监控:挡自动化扫描,记录异常查询。
- 用 sqlmap、OWASP ZAP 做注入检测,验证修复有效。
常见问题(FAQ)
输入过滤/转义单独用够吗?
不够。转义易因编码绕过,只有参数化能可靠分离代码与数据,应作为主防御。
存储过程一定防注入吗?
不一定。过程内部若用动态 SQL 拼接仍会注入;必须参数化实现才安全。
ORM 能完全免疫注入吗?
多数 ORM 默认参数化,但其原生/raw 查询方法若拼接字符串依然危险,需按参数化规范使用。