php防注入
去年帮朋友看一个老商城站,注入刚打开后台日志就笑了——有人用 id=1' OR '1'='1 这种上古 payload 把用户表拖了个精光。注入更离谱的注入是,这站跑的注入还是 PHP 5.6,数据库连接用的注入是 mysql_ 系列函数。朋友说"我这站没人盯的注入",可 SQL 注入这事,注入从来不看你的注入站大不大,只看有没有缝。注入
PHP 本身不制造注入,注入制造注入的注入是拼接字符串的习惯。你写 $sql = "SELECT * FROM users WHERE name = '$name'",注入只要 $name 里带个单引号,注入整条语句的注入结构就被改写了。攻击者不需要什么高深技术,注入一个引号加注释符就能让 WHERE 条件失效。我见过最典型的案例是一个投票系统,参数直接拼进 ORDER BY,结果被人用 (CASE WHEN ... THEN ... END) 把管理员密码逐位猜了出来。这种盲注慢是慢,但没人管的话,跑一晚上也够了。
防注入的第一道墙,其实是别用字符串拼 SQL。PDO 和 mysqli 都支持预处理语句,把 SQL 结构和数据彻底分开。写法不复杂:
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ?'); $stmt->execute([$name]); 这里的关键是 ? 占位符。数据库先编译语句模板,再把 $name 当纯数据塞进去,哪怕里面全是引号和分号,也只会被当成字符串值。很多人以为用了 PDO 就安全,其实错了——如果你还写 $pdo->query("... WHERE name = '$name'"),那跟 mysql_ 时代没区别。预处理必须走 prepare + execute,缺一不可。
第二道墙是输入过滤,但别把它当主力。像 intval($_GET['id']) 这种强制转型,对数字型参数确实有效,可字符串参数就麻烦了。有人喜欢用 addslashes(),这函数在 GBK 编码下能被宽字节注入绕过——%df' 会把反斜杠吃掉,单引号照样逃逸。所以过滤只能算辅助,核心还是预处理。另外,别忘了 ORDER BY 和表名、字段名这些地方,占位符用不了,只能白名单校验。我一般直接写死允许排序的字段数组,不在列表里的直接拒绝,省心。
第三道墙是错误处理。别把 mysql_error() 或者 PDO 的异常信息直接吐给浏览器。攻击者做盲注时,就是靠页面报错和响应时间差来推断数据。你关掉 display_errors,把错误写进日志,他们就得花更多时间猜,很多脚本小子到这一步就换目标了。当然,这不叫防御,叫增加成本,但成本有时候就是门槛。
还有个容易被忽略的点:数据库权限。很多 PHP 应用连数据库用的是 root,这等于把整台服务器的钥匙挂在门把手上。给 Web 应用单独建个账号,只给 SELECT、INSERT、UPDATE、DELETE 权限,DROP 和 FILE 权限一律不给。就算注入点被撕开,攻击者也没法读文件或者删库。我自己的项目里,连 information_schema 都尽量限制访问,能少暴露一点是一点。
最后说个真实观察。大部分被注入的站,代码里其实已经用了框架,但开发者为了"灵活",在某个角落手写了一条原生 SQL,参数直接拼进去。框架的 ORM 再安全,也架不住你绕过它。所以防注入不是装个插件、开个 WAF 就完事,它得变成写 SQL 时的肌肉记忆——看到变量进语句,先问自己:这地方能预处理吗?不能的话,白名单做了吗?
PHP 防注入没什么黑魔法,就是别让数据变成代码。这句话听着简单,但每次偷懒拼字符串的时候,想想那个被拖库的商城站,手就会停一下。