match against易被绕过预编译,因?占位符在against()中仅作字面量不参与解析,攻击者传入test" or 1=1/*等payload若未清洗即拼接,仍可触发注入;正确做法是语义清洗而非简单转义。

全文检索功能直接拼接用户输入到 SQL 的 MATCH AGAINST 或 LIKE 语句中,就是典型的 SQL 注入高危路径——这种写法在 MySQL 全文索引、Elasticsearch DSL 拼接、甚至自研关键词分词查询中反复出现,必须用参数化或上下文感知的转义替代字符串拼接。
为什么 MATCH AGAINST 容易被绕过预编译?
MySQL 的 MATCH(col) AGAINST(? IN NATURAL LANGUAGE MODE) 看似支持参数占位符,但实际只接受字面量字符串,? 在这里会被当作普通文本处理,不参与全文解析。攻击者传入 test" OR 1=1 /* 这类 payload,若后端未做剥离就直接塞进 AGAINST('xxx'),就会触发注入。
- MySQL 5.7+ 要求
AGAINST()内部必须是常量表达式,PDO 的bindValue()对它无效 - 常见错误写法:
"MATCH(title) AGAINST('" . $_GET['q'] . "' IN NATURAL LANGUAGE MODE)" - 正确做法不是“加引号+转义”,而是先清洗语义:移除引号、括号、布尔操作符(
+,-,>,, <code>~,*,OR,AND,NOT),再用htmlspecialchars()防 XSS,最后进 SQL
LIKE 查询中 % 和 _ 怎么安全转义?
当用 LIKE 做模糊搜索时,% 和 _ 是通配符,但用户输入里可能真有这两个字符(比如查型号 “A100%” 或 “user_2024”),直接 addslashes() 不起作用,必须显式指定 ESCAPE 字符并统一转义。
- PHP 中推荐:
$safe_q = str_replace(['%', '_'], ['%', '_'], $q);,然后 SQL 写成WHERE name LIKE ? ESCAPE '' - 绑定参数时用
bindValue(1, "%{$safe_q}%", PDO::PARAM_STR),确保不被二次解释 - 切勿用
mysql_real_escape_string()(已废弃)或mysqli_real_escape_string()处理 LIKE 场景,它们不处理 ESCAPE 逻辑
自定义分词或高亮逻辑里的隐藏风险
很多项目为支持中文分词,在 PHP 层手动拆词(如用空格、顿号、逗号分割),再拼成多个 OR title LIKE ? 条件。一旦分词后没对每个词单独过滤,攻击者传入 abc' OR '1'='1 就会污染整个 WHERE 子句。
- 分词后必须对每个子项单独调用
trim()+preg_replace('/['";\x00-\x1f\x7f]/u', '', $term) - 构造多条件时优先用
PDO::prepare()动态生成占位符,例如:WHERE title LIKE ? OR title LIKE ?,再批量bindValue() - 避免用
implode(' OR ', array_map(fn($t) => "title LIKE '$t'", $terms))这类字符串拼接
ES 或 Solr 查询 DSL 拼接同样危险
如果前端把搜索关键词直接拼进 query_string 或 match_phrase 的 JSON 字符串里,等同于重演 SQL 注入——JSON 中的双引号、反斜杠、控制字符都可能破坏结构,导致字段名/值被篡改。
- 永远用
json_encode($user_input, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)包裹用户输入,而不是字符串插值 - 禁用
query_string类型,改用match或multi_match,它们默认不执行布尔语法 - 服务端接收后,先用
filter_var($q, FILTER_SANITIZE_STRING)清洗,再进 DSL 构造流程
全文检索的注入点往往藏在“它看起来不像 SQL”的地方——MATCH、LIKE、DSL、分词结果、甚至 Lucene 解析器配置。最稳妥的做法不是靠某一层过滤,而是从接收 $_GET['q'] 的第一行代码开始,就把它当作不可信输入,每经过一次上下文转换(HTML 输出、SQL 构建、JSON 序列化),都必须做对应上下文的安全处理。漏掉任意一环,漏洞就成立。











