应使用prepare()与execute()实现参数化查询,将sql结构与用户数据彻底分离,杜绝字符串拼接;辅以输入验证、最小权限账户配置及错误信息隐藏等多重防御。

用 prepare() + execute() 替代字符串拼接
直接拼接用户输入到 SQL 字符串里,等于给攻击者递刀。哪怕只有一处 mysqli_query($conn, "SELECT * FROM users WHERE name = '$name'"),就可能被 admin' OR '1'='1 绕过登录。
正确做法是把 SQL 结构和数据彻底分开:
- PHP(PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :user"); $stmt->execute(['user' => $_POST['username']]); - PHP(MySQLi):
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?"); $stmt->bind_param("i", $id); $stmt->execute(); - Python(PyMySQL):
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
注意:占位符 ? 或 :key 不能用于表名、列名、ORDER BY 字段——这些必须走白名单校验,不能靠参数化兜底。
别信 mysqli_real_escape_string() 能防住一切
这个函数只是对单引号、反斜杠等做转义,但它解决不了多语句执行(如 ; 分隔)、宽字节注入、或绕过引号的布尔盲注。更关键的是:它要求连接字符集设置严格匹配,否则可能失效。
常见踩坑点:
- 没调用
set_charset()就用mysqli_real_escape_string(),宽字节场景下会“吃掉”转义符 - 对数字型参数也套用该函数,反而可能引入类型隐式转换漏洞
- 在非字符串上下文(比如
WHERE id = $id)里用它,根本不起作用
结论:它只能作为补救手段,绝不能替代参数化查询。
限制数据库账号权限,让漏洞“炸不响”
即使某条查询被注入成功,如果数据库账号只有 SELECT 权限,攻击者最多读点数据;没有 DROP、CREATE、FILE 权限,就无法删库、写 WebShell 或读取服务器文件。
实操建议:
- Web 应用专用账号,只授予所需最小权限:
GRANT SELECT, INSERT ON mydb.users TO 'webapp'@'localhost'; - 禁用
LOAD DATA INFILE和SELECT ... INTO OUTFILE,防止文件读写 - 避免使用
root或admin账号连接应用
权限收紧后,很多高危 payload(如 UNION SELECT LOAD_FILE('/etc/passwd'))会直接报错拒绝执行。
关闭详细错误提示,别给攻击者“说明书”
MySQL 默认错误信息(如 You have an error in your SQL syntax;... near 'xxx' at line 1)会暴露表名、字段名甚至 SQL 结构,极大降低盲注门槛。
线上环境必须做到:
- PHP 中关掉
display_errors,开启log_errors - MySQL 配置项
sql_mode加上STRICT_TRANS_TABLES,减少容忍型解析 - 应用层捕获异常后,只返回通用提示(如“请求失败”),错误细节记日志供排查
真正难防的不是注入本身,而是开发者在调试阶段留下的 var_dump($sql) 或开启的 show_sql_errors = true —— 这些往往比代码逻辑更早暴露攻击面。











