预处理语句(PreparedStatement)通过参数化查询严格区分SQL代码与用户数据,从根本上防止SQL注入;需配合最小权限数据库账号(禁用DDL权限)及白名单机制防御动态SQL风险。

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










