预处理语句(PreparedStatement)通过参数化查询严格区分SQL代码与用户数据,从根本上防止SQL注入;需配合最小权限数据库账号(禁用DDL权限)及白名单机制防御动态SQL风险。
用预处理语句(Prepared Statement)代替字符串拼接
SQL注入的本质是用户输入被当作 SQL 代码执行。最直接有效的防御,就是让数据库明确区分「代码」和「数据」——
正是干这个的。
常见错误现象:
这种写法,一旦
是
,就完蛋。
Java 中必须用
,参数全走
等方法,绝不用
拼接
Python 的
、
,PHP 的
同理:占位符用
或
,值单独传入
ORM 如 MyBatis 的
是安全的,但
是字符串替换,等同于拼接,禁用
注意:预处理只防 DML(SELECT/INSERT/UPDATE/DELETE),不防 DDL(CREATE/DROP)——它本身不解决「禁止 DDL」问题
应用数据库账号权限最小化:不给 DDL 权限
即使 SQL 注入得手,如果账号连
都执行不了,危害就锁死在数据读写层面。
使用场景:部署时创建专用应用账号,不是用
或
连接生产库。
MySQL:建账号后只授
,显式跳过
,
,
,
PostgreSQL:用
,不给
、
,且确保不在
上授权
注意:有些 ORM(如 Django 的
)或监控工具会尝试查
,可单独授
权限,但绝不给写
验证方式:用该账号登录后执行
,应报错
拦截非法 DDL 关键词(仅作兜底,不可依赖)
权限控制是主防线,关键词过滤只是辅助——因为绕过方式太多(大小写混写、注释拆分、编码混淆),但某些中间件或 WAF 层面仍会加这层。
容易踩的坑:正则写太松会误杀正常字段名(比如字段叫
),写太紧又拦不住
。
若必须做,匹配应区分上下文:只在
字符串首部或独立语句位置检查
禁止用
这类无上下文判断——
就挂了
更稳妥的做法是在数据库代理层(如 ProxySQL、pgBouncer)配置规则,或用数据库审计插件(如 MySQL Enterprise Firewall)
警惕存储过程与动态 SQL 的隐式 DDL 风险
很多人以为「没写
就安全」,但存储过程中可能藏 DDL;或者应用里用
、
拼接 SQL,等于自己造了个注入入口。
性能与兼容性影响:这类逻辑通常难审计、难测试,且不同数据库对动态 SQL 的权限校验时机不一致(有的在编译时,有的在执行时)。
MySQL 存储过程中禁止出现
+
组合;PostgreSQL 中避免
拼接用户输入
如果业务真需要动态表名(如分表),改用白名单映射:
,而非直接拼接
所有含
、
、
的代码,必须人工逐行 review 输入来源
真正麻烦的不是「怎么禁 DDL」,而是团队里有人觉得「反正有防火墙」「反正只内网」就跳过权限隔离——DDL 权限一旦放开,一个注入点就能清空整个库,而且日志里可能只留下一条
记录。
PreparedStatementString sql = "SELECT * FROM users WHERE name = '" + userName + "'";userName' OR '1'='1Connection.prepareStatement()setString()+sqlite3psycopg2PDO::prepare()?:name#{}${}DROP TABLErootpostgresSELECT, INSERT, UPDATE, DELETECREATEDROPALTERGRANT OPTIONGRANT SELECT, INSERT, UPDATE, DELETE ON TABLESCREATEROLECREATEDBpg_catalogmigrateinformation_schemaSELECTDROP TABLE IF EXISTS test;ERROR: permission denied for relation testcreate_time/*abc*/DROP/*def*/TABLEsql^(?i)(CREATE|DROP|ALTER|TRUNCATE|RENAME)String.contains("DROP")"user_name".contains("DROP")DROPEXECUTE IMMEDIATEsp_executesqlPREPAREEXECUTEEXECUTEif (tableSuffix.equals("2024")) { tableName = "log_2024"; }EXECUTEsp_executesqlEXECUTE IMMEDIATESELECT