SQL注入防护如何实现前后端联动,构建输入验证与输出编码的双重防线?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1153个文字,预计阅读时间需要5分钟。
SQL注:
前端校验不是摆设,但它的作用很明确
前端的输入限制(比如oninput 过滤、pattern 属性、表单提交前 RegExp.test())本质是用户体验优化和初级过滤,**不提供任何安全保证**。
- 浏览器禁用、开发者工具绕过、抓包重放、移动端 WebView 注入都能轻松跳过前端校验
- 但它能快速拦截明显非法输入(如 ' OR '1'='1、UNION SELECT),减少无效请求打到后端
- 若配合 Content-Security-Policy 阻止内联脚本,还能削弱 DOM 型 XSS 辅助 SQL 注入的路径
建议做法:
- 对敏感字段(用户名、搜索关键词、ID 查询参数)做基础白名单过滤,例如只允许
[a-zA-Z0-9_-@. ] - 不要依赖
maxlength或type="number"拦截恶意 payload,它们可被轻易 bypass - 所有提交数据仍必须视为“不可信”,后端绝不信任
req.body或req.query的原始值
后端才是 SQL 注入的主战场,但校验位置不能错
SQL注入 的发生前提是:用户可控输入 → 被拼进 SQL 字符串 → 交由数据库执行。所以防御核心是**切断拼接环节**,而不是事后清洗。
- PreparedStatement(Java)、parameterized query(Python/Node.js)是事实标准,它把参数与 SQL 结构分离,数据库引擎天然拒绝执行参数中的指令
- 错误做法包括:String.format() 拼接 SQL、concat() 构造查询、用 replaceAll("'", "''") 手动转义——这些都可能被多字节编码、宽字节注入或注释符绕过
- 动态表名/列名无法用参数化?那就用白名单映射,比如 sortFieldMap.get("created_at") 返回 "created_time",而非直接插字符串
关键细节:
- 即使用了
PreparedStatement,也要对参数做类型校验(如 ID 必须是正整数,用Long.parseLong()+ 异常捕获) - 日志中记录原始请求参数时,避免直接写入未脱敏的
req.query.q,防止日志注入(log4j 类漏洞) - 数据库账号必须遵循最小权限原则,应用账号不应有
DROP、CREATE、EXECUTE权限
为什么“输出编码”在这里不适用?别混淆 XSS 和 SQL 注入
有人把防XSS 的 HTML entity encode 思路套到 SQL 上,这是典型误区。
- encodeURI()、encodeURIComponent() 是为 URL 安全设计的,对 SQL 解析器完全无效
- escapeHtml4() 或 Jsoup.clean() 处理的是 HTML 渲染上下文,不是数据库查询上下文
- 把用户输入先 HTML 编码再塞进 SQL,反而可能导致业务逻辑错误(比如搜索“C++”变成 “C%2B%2B”,查不到结果)
记住一点:
- 输入进入数据库前,用参数化机制隔离;
- 输出到浏览器前,用
HtmlUtils.htmlEscape()或模板引擎自动转义(如 Thymeleaf 的th:text); - 两者上下文不同,混用等于没防。
协同失效的常见断点
前后端看似都做了防护,但实际仍被注入,往往卡在这几个地方: - 前端校验松散(比如邮箱正则太宽,允许admin'-- 通过),后端又没二次校验类型和长度
- 后端用了 MyBatis 的 ${} 语法拼接动态 SQL,而没意识到它等价于字符串拼接
- API 接口接受 JSON 数组,后端用 for 循环拼 IN (?),但没对每个元素单独参数化
- 前端发了 GET /api/users?id=1,2,3,后端解析成数组后,用 String.join(",", ids) 拼进 SQL
最易被忽略的一点:所有外部输入源都要覆盖——不只是 req.query 和 req.body,还包括 req.headers(如 X-Forwarded-For 伪造 IP 后注入日志 SQL)、req.params(RESTful 路径参数)、甚至缓存键名或消息队列 payload。
本文共计1153个文字,预计阅读时间需要5分钟。
SQL注:
前端校验不是摆设,但它的作用很明确
前端的输入限制(比如oninput 过滤、pattern 属性、表单提交前 RegExp.test())本质是用户体验优化和初级过滤,**不提供任何安全保证**。
- 浏览器禁用、开发者工具绕过、抓包重放、移动端 WebView 注入都能轻松跳过前端校验
- 但它能快速拦截明显非法输入(如 ' OR '1'='1、UNION SELECT),减少无效请求打到后端
- 若配合 Content-Security-Policy 阻止内联脚本,还能削弱 DOM 型 XSS 辅助 SQL 注入的路径
建议做法:
- 对敏感字段(用户名、搜索关键词、ID 查询参数)做基础白名单过滤,例如只允许
[a-zA-Z0-9_-@. ] - 不要依赖
maxlength或type="number"拦截恶意 payload,它们可被轻易 bypass - 所有提交数据仍必须视为“不可信”,后端绝不信任
req.body或req.query的原始值
后端才是 SQL 注入的主战场,但校验位置不能错
SQL注入 的发生前提是:用户可控输入 → 被拼进 SQL 字符串 → 交由数据库执行。所以防御核心是**切断拼接环节**,而不是事后清洗。
- PreparedStatement(Java)、parameterized query(Python/Node.js)是事实标准,它把参数与 SQL 结构分离,数据库引擎天然拒绝执行参数中的指令
- 错误做法包括:String.format() 拼接 SQL、concat() 构造查询、用 replaceAll("'", "''") 手动转义——这些都可能被多字节编码、宽字节注入或注释符绕过
- 动态表名/列名无法用参数化?那就用白名单映射,比如 sortFieldMap.get("created_at") 返回 "created_time",而非直接插字符串
关键细节:
- 即使用了
PreparedStatement,也要对参数做类型校验(如 ID 必须是正整数,用Long.parseLong()+ 异常捕获) - 日志中记录原始请求参数时,避免直接写入未脱敏的
req.query.q,防止日志注入(log4j 类漏洞) - 数据库账号必须遵循最小权限原则,应用账号不应有
DROP、CREATE、EXECUTE权限
为什么“输出编码”在这里不适用?别混淆 XSS 和 SQL 注入
有人把防XSS 的 HTML entity encode 思路套到 SQL 上,这是典型误区。
- encodeURI()、encodeURIComponent() 是为 URL 安全设计的,对 SQL 解析器完全无效
- escapeHtml4() 或 Jsoup.clean() 处理的是 HTML 渲染上下文,不是数据库查询上下文
- 把用户输入先 HTML 编码再塞进 SQL,反而可能导致业务逻辑错误(比如搜索“C++”变成 “C%2B%2B”,查不到结果)
记住一点:
- 输入进入数据库前,用参数化机制隔离;
- 输出到浏览器前,用
HtmlUtils.htmlEscape()或模板引擎自动转义(如 Thymeleaf 的th:text); - 两者上下文不同,混用等于没防。
协同失效的常见断点
前后端看似都做了防护,但实际仍被注入,往往卡在这几个地方: - 前端校验松散(比如邮箱正则太宽,允许admin'-- 通过),后端又没二次校验类型和长度
- 后端用了 MyBatis 的 ${} 语法拼接动态 SQL,而没意识到它等价于字符串拼接
- API 接口接受 JSON 数组,后端用 for 循环拼 IN (?),但没对每个元素单独参数化
- 前端发了 GET /api/users?id=1,2,3,后端解析成数组后,用 String.join(",", ids) 拼进 SQL
最易被忽略的一点:所有外部输入源都要覆盖——不只是 req.query 和 req.body,还包括 req.headers(如 X-Forwarded-For 伪造 IP 后注入日志 SQL)、req.params(RESTful 路径参数)、甚至缓存键名或消息队列 payload。

