应使用预处理语句替代sql拼接,严格遵循最小权限原则,清理默认危险配置,并在应用层增加输入白名单与类型强校验。

用预处理语句(Prepared Statements)代替拼接SQL
直接拼接用户输入到 SELECT、INSERT 等语句中,是 SQL 注入最常见入口。MySQL 本身不校验字符串内容,只按语法执行——只要注入的 SQL 片段语法合法,就会被执行。
正确做法是使用客户端驱动层的预处理机制,让 MySQL Server 分离「SQL 结构」和「数据值」:
- PHP 中用
PDO::prepare()+bindValue(),不是mysql_query()(已废弃) - Python 中用
cursor.execute("SELECT * FROM user WHERE id = %s", [user_id]),不用f"WHERE id = {user_id}" - Java 中用
PreparedStatement,参数用?占位,调用setString()等方法赋值
注意:仅用 mysqli_real_escape_string() 或正则过滤是靠不住的——它无法覆盖所有上下文(比如数字上下文、JSON 字段、ORDER BY 子句),且容易漏掉多字节编码绕过场景。
最小权限原则:给应用账号配专属数据库用户
别让 Web 应用连的是 root@localhost。一个典型误操作是建库后直接 GRANT ALL ON *.* TO 'app'@'%',这等于把整套实例权限交出去。
应按实际需求限制:
- 只读接口 → 只授
SELECT权限,且限定到具体库表,如GRANT SELECT ON myapp.users TO 'web_ro'@'10.0.1.%' - 写操作分离 → 写账号禁用
DROP、ALTER、CREATE,甚至禁用DELETE(用软删字段替代) - 禁止跨库访问 → 不用
myapp.*,而用myapp.user_profile、myapp.order_log明确授权 - 连接来源限制 → 把
'%'换成内网 IP 段,如'172.16.20.%',避免暴露到公网
权限变更后务必执行 FLUSH PRIVILEGES,否则新规则不生效。
关闭危险配置与默认高危账户
MySQL 默认安装常带安全隐患,需手动清理:
- 删除匿名用户:
DROP USER ''@'localhost' - 禁用
LOAD DATA INFILE和SELECT ... INTO OUTFILE:在my.cnf中设secure_file_priv = ""或指向空目录,防止文件读写类注入利用 - 关闭旧协议认证:
default_authentication_plugin = caching_sha2_password(5.7.21+ / 8.0+),避免mysql_native_password的弱协商风险 - 禁用远程
root登录:DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1'),再FLUSH PRIVILEGES
这些配置改完要重启 mysqld 或执行 SET GLOBAL(部分可动态生效),但 secure_file_priv 必须重启才生效。
应用层加一道校验:输入白名单 + 类型强约束
数据库权限和预处理是底线,但不能全依赖它们。比如 ID 参数本该是正整数,就应在 PHP/Python 中先做 is_int() 或 int(user_input) 转换;枚举字段如 status,应查表或硬编码白名单,而非直接代入 SQL。
常见疏漏点:
-
ORDER BY子句无法用预处理参数(MySQL 不允许占位符用于列名/关键字),必须用白名单映射:$allowed_sort = ['created_at', 'score']; $sort = in_array($_GET['sort'], $allowed_sort) ? $_GET['sort'] : 'created_at'; - LIKE 模糊查询中的通配符
%和_需转义,用ESCAPE子句或客户端函数(如 Python 的re.escape()后替换) - JSON 字段内容若含用户输入,需先 JSON 解码再校验结构,不能直接拼进
JSON_EXTRACT()表达式
真正难防的从来不是复杂注入手法,而是开发时觉得“这里就一个数字ID,肯定安全”,结果忘了类型转换或边界检查。










