MySQL解析器不识别SQL注入,仅做语法校验;真正防御靠PreparedStatement的参数隔离机制,将SQL模板与参数分离传输,使用户输入永不参与解析。

MySQL解析器不会主动识别SQL注入风险
这里有个常见的误解需要澄清:解析器的工作,仅仅是进行语法校验并生成执行计划。它可不会去判断你那条 SELECT * FROM users WHERE id = ? 到底是合法的用户查询,还是精心伪装的攻击载荷。所谓的“识别注入”,其实是应用层该操心的事。举个例子,如果你在代码里直接拼接了用户输入:"SELECT * FROM users WHERE name = '" + userInput + "'",解析器会照单全收,只要语法正确,它就继续往下执行。真正能拦住注入的,其实是预处理语句(PreparedStatement)背后,由客户端和服务端协作完成的参数隔离机制。
PreparedStatement 执行时发生了什么
这个过程可不是“先拼接SQL再执行”那么简单,而是分两步走:第一步,客户端将带有占位符的SQL模板(比如 INSERT INTO log(msg) VALUES(?))发送给MySQL服务端,服务端会编译这个模板并缓存其执行计划;第二步,客户端只传输参数的二进制值(比如字符串 "' OR 1=1 -- "),这些值自始至终都被当作纯粹的数据来处理,压根不会进入SQL解析阶段。
- 服务端收到
COM_STMT_PREPARE指令包后,会调用parse_sql()来解析模板。但关键点在于,此时参数值是缺失的,无法构成一条完整的可执行语句,因此也就不存在所谓的“注入上下文”。 - 后续的
COM_STMT_EXECUTE指令包里,只包含了参数的类型、长度和原始字节流。MySQL直接将这些值绑定到之前已编译好的执行计划中,完全跳过了词法分析和语法树重构的过程。 - 这样一来,即便参数里包含了
;、--、/*这类特殊字符,它们也不会触发语句截断或注释解析——原因很简单,这些字符根本就没被当作SQL字符串的一部分来解析。
为什么 prepare + execute 组合才安全,单独用 prepare 不行
这里有个微妙的区别。prepare 本身只是MySQL的一个命令。如果在客户端,你仍然用字符串拼接的方式,构造出一个包含恶意内容的“模板”(例如 "SELECT * FROM t WHERE id = " + userInput),然后再交给 PREPARE stmt FROM @sql 去执行,那么注入其实已经发生在模板的生成阶段了。真正的安全边界,其实在于编程语言提供的 PreparedStatement API(比如Ja va的 Connection.prepareStatement())。这套API强制性地将SQL模板和参数分离开,并通过协议级别的独立字段来传输参数值。
- 以Ja va为例,
ps.setString(1, userInput)发送的,是一个独立的、类型标记为MYSQL_TYPE_STRING的参数包,而不是简单的字符串插值。 - PHP的PDO同理,
$pdo->prepare("SELECT ... ?")配合execute([$userInput]),走的也是二进制协议(binary protocol)的参数通道。 - 反过来,如果直接使用MySQL原生命令,例如
SET @s = CONCAT('SELECT * FROM t WHERE x=', @inj); PREPARE stmt FROM @s;,那就等于自己动手,绕开了所有的防护机制。
容易被忽略的“伪安全”场景
有些写法看起来像是用了预处理,实际上安全机制并未生效。典型的场景就是动态表名、列名或者排序字段——这些位置无法使用 ? 占位符,只能通过字符串拼接来实现。一旦这些动态内容的来源不可信,防线立刻就会被攻破。
SELECT * FROM ?是语法错误,MySQL不允许占位符出现在表名或列名这类标识符的位置。ORDER BY ?虽然能通过prepare阶段,但传入的参数会被当作字符串字面量处理(例如变成ORDER BY 'name'),而不是作为列标识符来解析,结果很可能不符合预期。- 试图在SQL语句内部,使用
CONCAT()或FORMAT()等函数来拼接字段名,这相当于把注入风险从应用层转移到了数据库层,本质上依然危险。
说到底,参数化查询能保护的是“值”(value),保护不了“结构”(identifier)。对于SQL语句的结构部分,必须通过白名单校验或硬编码的方式来确保安全。
