参数化查询是唯一可靠起点,因它在数据库解析阶段就分离sql模板与用户数据,使恶意输入如admin' or 1=1 --仅被视作字符串值,不触发词法分析截断或注释绕过。

不能只靠WAF或输入过滤,必须从SQL执行层切断数据与代码的混同——参数化查询是唯一可靠起点。
为什么预处理语句能真正拦住注入
预处理语句不是“加一层过滤”,而是让数据库在解析阶段就明确:这部分是模板结构,这部分是纯数据。哪怕用户输入admin' OR 1=1 --,绑定到?或:username后,数据库只会把它当字符串值存进WHERE条件,不会触发词法分析中的单引号截断、注释吞掉后续逻辑等行为。
常见错误现象包括:用mysqli_real_escape_string()转义后仍拼接SQL;前端JS校验后端不做二次验证;WAF拦截了UNION SELECT但放行EXTRACTVALUE报错注入——这些都绕不开一个事实:只要SQL字符串里还存在用户输入的拼接痕迹,攻击面就始终存在。
- PHP PDO中必须用
prepare()+execute()组合,不能用query()直接执行拼接串 - Java里
PreparedStatement的setString()等方法必须按位置/名称绑定,不能用Statement拼接 - Python MySQLdb或PyMySQL中,
cursor.execute("SELECT * FROM t WHERE id = %s", (user_id,))才是安全写法,%格式化或f-string拼接一律禁用
存储过程不是银弹,用错反而更危险
很多人以为把SQL逻辑搬到数据库里写成存储过程就能防注入,这是误解。如果存储过程中用了CONCAT()或EXECUTE IMMEDIATE动态拼接用户输入,照样会出问题。MySQL的存储过程本身不自动隔离输入,它只是把拼接动作从应用层挪到了数据库层。
真正安全的存储过程写法,是所有参数都作为IN参数传入,并在内部用原生WHERE条件直接引用,例如:WHERE username = p_username。一旦出现SET @sql = CONCAT('SELECT * FROM users WHERE ', p_condition)这种写法,风险等级和应用层拼接无异。
- 检查现有存储过程是否含
PREPARE/EXECUTE语句 - 确认所有外部输入都通过
IN参数传入,且未参与字符串拼接 - 避免在存储过程中调用
sys_eval或load_file等高危函数(即使权限已限制)
最小权限原则必须落实到具体SQL操作
即便用了参数化查询,如果数据库账号拥有DROP、ALTER、FILE权限,攻击者一旦通过其他漏洞(如日志文件读取、UDF提权)拿到数据库连接,仍可能升级为服务器权限。所以应用连接账号不能是root,也不能是只读账号配SELECT之外的权限。
典型错误配置是给Web应用分配了web_user@'%'并授予ALL PRIVILEGES,这等于把数据库的“钥匙串”直接交出去。而真实生产环境应按模块切分权限,比如后台管理模块账号只允许SELECT, UPDATE, DELETE指定几张表,报表模块账号仅限SELECT视图。
- 用
SHOW GRANTS FOR 'web_user'@'localhost';确认实际权限 - 删除
FILE权限(防止SELECT ... INTO OUTFILE写Webshell) - 禁用
LOAD DATA INFILE和SOURCE命令(除非业务强依赖) - 限制账号可连接的IP范围,如
'web_user'@'10.0.1.%'
最易被忽略的一点是:参数化查询只解决“查”和“改”的注入,但无法阻止攻击者利用SELECT结果里的路径、配置项、日志内容去发起二次攻击。比如查出/var/www/html/config.php,再结合文件读取漏洞拿源码;或者查到backup.sql.gz路径,直接下载脱库。防御必须延伸到数据可见性控制和操作行为审计上。











