必须用 preparedstatement 替换字符串拼接来防御万能密码漏洞,因其将参数与 sql 结构分离,自动转义恶意字符;表名字段名需白名单校验,orm 中禁用 ${} 和 raw 查询,且登录须单条参数化 sql 联合验证密码。

直接用 PreparedStatement 替换字符串拼接
几乎所有万能密码漏洞(如 ' or 1=1 --、' or '1'='1)都源于后端把用户输入直接拼进 SQL 字符串。Java 中最常见错误就是用 Statement + 字符串拼接,比如:"SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"。这种写法让单引号、注释符、逻辑运算符全被数据库原样执行。
修复方式不是“过滤单引号”,而是彻底避免拼接。必须改用 PreparedStatement,让参数与 SQL 结构分离:
String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); // 用户输入走参数绑定 ps.setString(2, password); // 不参与SQL语法解析 ResultSet rs = ps.executeQuery();
- 数据库驱动会自动对
?占位符的值做类型安全转义,'、--、#全部失去语法意义 - 不要试图在
PreparedStatement里拼表名或字段名——这些不能参数化,需白名单校验 - 如果用了 ORM(如 MyBatis),确保没用
${}拼接,只用#{};Hibernate 则禁用createSQLQuery(),优先用 HQL 或 Criteria API
别信“Replace 单引号”这类表面过滤
很多老项目用 Replace(username, "'", "''") 或正则删掉 --、#,这根本不可靠。攻击者可以绕过:
- 用
/**/替代空格,绕过空格检测 - 用十六进制编码(如
0x27表示')逃逸字符过滤 - 在 MySQL 中,
/*!50000 SELECT */这类注释可执行任意语句 - Oracle 支持
||字符串连接,chr(39)动态生成单引号
过滤永远追不上攻击变种,它只是给漏洞披了层薄纱。真正防线是参数化查询本身,不是拦住恶意字符。
登录逻辑必须校验密码,不能只查用户名存在
有些修复只加了参数化,但业务逻辑仍有缺陷:比如先查 username 是否存在,再单独比对密码。这会导致两个问题:
- 用户名枚举:攻击者通过响应时间或错误信息判断账号是否存在
- 绕过风险:若第一步查询返回用户数据,第二步密码校验被跳过(如异常未捕获、条件分支错乱),仍可能登录成功
正确做法是:一条参数化 SQL 完成“用户名+密码”联合验证,且密码必须用安全哈希(如 bcrypt)比对,不能明文或弱哈希(MD5/SHA1)。
额外注意:框架默认配置可能留后门
Spring Boot、Django 等现代框架虽默认推荐参数化,但仍有陷阱:
- MyBatis 的
<script></script>标签或bind语句若拼接用户输入,照样注入 - Django 的
extra()、raw()查询不经过 ORM 参数化,需手动确认输入可信 - Node.js 的
pg库若用client.query("SELECT * FROM u WHERE n = $1", [input])是安全的,但用client.query(`SELECT * FROM u WHERE n = '${input}'`)就崩了
参数化不是一劳永逸,关键看每一处 SQL 执行是否真正隔离了用户输入。只要出现字符串插值(`...${input}...`、"..." + input),就得重写。










