2026/10/10 12:51:54

PHP+MySQL安全实战:SQL注入、跨库查询与文件读写防护指南

PHP+MySQL安全实战:SQL注入、跨库查询与文件读写防护指南 做Web安全测试这些年“SQL注入”这个词听得最多但很多朋友到了真正面对PHPMySQL组合的时候反而容易懵——明明“万能密码”能用换个场景就失效明明知道有注入点却不知道还能跨库读数据拿到数据库权限后文件读写又受各种限制。这其实不是技术没学会而是对应用架构和数据库权限模型的理解不够。这篇文章基于我实际测试中的经验把PHP应用、MySQL架构、SQL注入、跨库查询、文件读写和权限操作放在一起讲透希望能帮正在学安全测试或做PHP开发的朋友把底层逻辑理顺。1. 先从PHPMySQL的架构说起1.1 一套Web系统是怎么跑起来的我们在分析任何PHP应用的数据库问题时第一件事不是急着去找注入参数而是先搞清楚整个链路浏览器发起请求Nginx或Apache把请求交给PHP解释器PHP脚本通过MySQL客户端库mysqli、PDO或老旧的mysql扩展连接数据库数据库返回结果PHP再渲染成页面。这条链路里真正决定SQL注入能否成功、跨库查询能不能执行、文件读写要不要理的是PHP脚本里怎么写SQL、以什么身份连数据库、MySQL实例上有哪些库和权限。举个例子某套CMS的安装配置里通常会要求填写数据库地址、账号、密码。如果开发者图省事直接用了root账号并且把密码写死在config.php里那整个应用的所有SQL操作都是最高权限。这种情况下一旦某个入口存在拼接漏洞攻击者能做的事就远不止“绕过登录”了他可以直接操作整个MySQL实例的所有数据库甚至通过文件读写往服务器上写文件。所以我经常说SQL注入的“天花板”不完全取决于漏洞本身而是取决于数据库账号的权限。PHP这边连接方式也分三六九等。mysqli和PDO都支持预处理语句这是防御注入的正道而老代码里常见的mysql_query拼接字符串则是漏洞温床。还有个容易忽略的点PHP和MySQL之间的字符集处理不当可能造成宽字节注入这类问题在GBK环境下特别典型。后面我会专门提一句。1.2 数据库权限模型决定了攻击范围MySQL的权限模型是“用户 主机 库表 权限类型”的组合。你在创建账号时写的是appuserlocalhost还是appuser%决定了连接来源你授予的是SELECT ON db1.*还是ALL PRIVILEGES ON *.*决定了能看多少数据。跨库查询之所以值得关注是因为很多应用虽然业务逻辑上只属于一个库但同一台MySQL服务器上可能同时跑着多个业务库、多个应用共用一个实例。如果PHP连接账号的权限是全局的那么注入后就能跨库访问其他应用的敏感数据。这在国内一些中小公司尤其常见为了省服务器资源所有项目共用一个MySQL实例库里连支付信息、用户手机号、后台管理员账号都存在一起。安全测试到了这一步影响面就从“一个网站”扩大到了“一台服务器上所有人”。文件读写则依赖MySQL的FILE权限。这个权限不是默认给普通业务账号的但如果你用了root或者手工授权时给了FILE那么配合SQL注入就可能通过LOAD_FILE()读取服务器上的配置文件或者通过INTO OUTFILE往Web目录写入脚本。后面第3章我会详细拆解。2. SQL注入的本质与常见入口2.1 先理解注入是怎么发生的SQL注入说白了就是“用户输入被拼进了SQL语句并且改变了SQL原本的语义”。比如后端有一句$id $_GET[id]; $sql SELECT * FROM news WHERE id . $id; $result mysqli_query($conn, $sql);正常请求?id1时SQL是SELECT * FROM news WHERE id 1。但如果请求?id1 or 11SQL就变成了SELECT * FROM news WHERE id 1 or 11因为or 11永远成立所以会返回整个表的数据。这就是最基础的数字型注入。如果是字符型通常SQL长这样$name $_POST[username]; $pwd $_POST[password]; $sql SELECT * FROM users WHERE username $name AND password $pwd;这里变量被单引号包着攻击者需要先闭合引号再构造逻辑。网上常说的“万能密码”其实就是利用了这类拼接。比如用户名输入一个admin or 11密码随便填整条SQL就变成了SELECT * FROM users WHERE username admin or 11 AND password xxx由于or的优先级只要usernameadmin成立或者11成立查询就返回记录登录校验就被绕过。理解这个原理后你会发现“万能密码”能不能用取决于代码拼不拼接引号、有没有做过滤而不是网上那些固定payload“通吃”。2.2 不光GET参数能注入这几个入口常被忽略我审计过不少PHP项目最常见的注入点确实在$_GET[id]这类参数上但以下入口同样危险而且更容易被开发者忽略搜索框搜索功能通常要模糊查询代码里很喜欢写成LIKE %$keyword%如果直接拼接注入点就出现了。登录框和JSON接口现在很多前端用Ajax提交JSON后端直接json_decode($_POST[data])后取值拼SQL漏过滤的情况很常见。Cookie和Header比如记住登录状态、统计来源、记录客户端IP等把$_COOKIE[user]或$_SERVER[HTTP_USER_AGENT]拼进SQL。排序字段和分页参数这类参数经常是字符白名单但开发图方便直接拼字段名导致注入。二次注入用户输入先存库后面某个功能再拿出来拼SQL。特别是后台管理功能很多人在录入阶段没过滤等到编辑、导出时引发问题。识别注入点最笨也最有效的办法就是捋代码搜索mysqli_query、$pdo-query、-prepare等函数看变量来源是不是外部可控再看有没有经过intval、addslashes、htmlspecialchars或预处理。如果变量直接进了SQL字符串就要重点怀疑。2.3 参数化查询为什么是防注入的“正解”很多人喜欢用黑名单过滤比如屏蔽、、and、or、select等关键词。但这类过滤天然有缺陷编码绕过、大小写绕过、注释符拆分、等价函数替换绕过姿势一抓一大把。更关键的是过滤逻辑会跟业务需求冲突比如搜索功能里用户本来就要输入or或特殊字符你总不能全拦了。参数化查询预处理的核心思路是把SQL语句的结构和参数分开传递。数据库先编译SQL模板再把用户输入当作纯数据绑定进去这样无论输入什么都不会改变SQL结构。以PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE username :name AND password :pwd); $stmt-execute([:name $name, :pwd $pwd]);执行时就算$name是admin or 11它也只是作为一个字符串值去匹配不可能改变判断逻辑。这是我给所有PHP开发者的第一条建议所有SQL语句必须走预处理不允许字符串拼接变量。3. 跨库查询与文件读写、权限操作3.1 跨库查询的原理一台服务器上的数据互通MySQL里的“跨库查询”并不神秘。同一台MySQL实例可以创建多个数据库只要有权限一条SQL就能同时访问多个库的表比如SELECT * FROM db1.users JOIN db2.orders ON db1.users.id db2.orders.user_id;在没有权限时MySQL会直接报错ERROR 1142 (42000): SELECT command denied。但如果是root或全局权限账号这条语句就能跑通。安全测试中我们关注跨库查询是为了评估“如果这个应用被注入数据库账号能读到哪些数据”。注入成功时攻击者可以通过UNION SELECT或者报错注入去读取当前数据库的元数据再通过information_schema查所有库名、表名、字段名。information_schema是MySQL自带的“字典库”它记录了整个实例的所有信息普通用户通常也有权限读取部分内容比如自己库里的表但如果权限允许就能看到全部。这也是为什么我反复强调“业务账号绝不使用全局权限”。我给某高校的模拟项目做安全加固时发现他们的选课系统居然用root连接数据库一注入就能把同一台服务器上的图书系统、教务系统的库表全列出来影响面极其恐怖。后来把每个系统拆成独立账号只授予本库的SELECT、INSERT、UPDATE、DELETE跨库查询的通道才算堵上。3.2 文件读取和写入条件比你想的苛刻数据库能读写文件是很多初学安全的朋友特别感兴趣的点但实际测试中受限非常多。先看读取MySQL提供LOAD_FILE()函数可以读取服务器上的文本文件。但前提是当前数据库账号有FILE权限secure_file_priv变量未限制或者文件路径在允许范围内操作系统层面MySQL进程对目标文件有读权限。secure_file_priv是MySQL的导入导出限制变量。它有三种情况NULL表示禁止导入导出空字符串表示不限制指定目录表示只允许在该目录操作。很多发行版默认配置了空字符串或NULL这就导致LOAD_FILE()经常被拒。看看这个函数怎么用SELECT LOAD_FILE(/etc/passwd); SELECT LOAD_FILE(/var/www/html/config.php);如果配置允许LOAD_FILE可以直接把服务器上敏感文件的内容查出来。在PHPMySQL架构下config.php通常包含数据库账号密码、密钥等一旦被读出来这个应用基本就沦陷了。文件写入的情况更复杂。常用的是INTO OUTFILE和INTO DUMPFILE。OUTFILE会把查询结果写到服务器文件系统SELECT ?php eval($_POST[1]); ? INTO OUTFILE /var/www/html/shell.php;但要让这条语句成功除了账号权限和secure_file_priv限制外还需要Web根目录对MySQL进程可写目标文件不能已存在OUTFILE会拒绝覆盖现有文件系统不能开启open_basedir把PHP限制死了虽然这不影响MySQL写文件。所以文件写入的利用难度远高于读取。很多测试者上来就执行INTO OUTFILE结果报错就以为“没法写文件”其实可能是secure_file_priv没放开也可能是路径写错了。对于开发者来说防御思路很明确创建业务账号时默认不授予FILE权限。除非有特别明确的导出需求否则永远不要给业务账号留文件操作能力。3.3 权限操作最小权限才是安全基线“权限操作”这四个字在标题里看着像“利用”但实际上更多是“配置与审计”。我见过的数据库安全事故八成以上源于权限过大。这里我给出一套可落地的账号权限分配方案场景建议权限说明Web业务库账号仅本库的SELECT、INSERT、UPDATE、DELETE不做DDL不跨库不读文件备份账号SELECT、LOCK TABLES或RELOAD能读数据但不需要写权限运维/管理账号按需分配但不与业务混淆单独使用不在应用配置里出现只读分析账号本库或多库SELECT用于报表不能写数据在实际环境中我建议至少做到以下几点不要使用root连接应用每个应用一个独立数据库账号密码由单独的配置管理禁止授予FILE、PROCESS、SUPER等高级权限定期检查mysql.user、mysql.db表回收不再使用的账号和权限打开MySQL的general_log或使用审计插件记录所有SQL操作方便事后排查。权限的最小化看起来只是配置项实际是防线。没有高权限注入的破坏力就停留在“当前库当前表”跨库查询和文件读写都会被直接卡死。这也是安全测试里评估“危害等级”的重要依据。4. 实操排查与防护落地4.1 巡检SQL注入从哪里下手如果你是安全测试人员拿到一套PHP源码或一个线上应用建议按下面顺序做一轮排查全局搜索危险函数mysqli_query、mysql_query、PDO::query、-exec以及prepare。看这些语句是否由外部参数拼接而成搜索$_GET、$_POST、$_REQUEST、$_COOKIE、$_SERVER。对每一处拼接点人工构造测试用例普通参数、单引号、布尔条件、时间盲注等但测试时一定要在授权范围内避免对线上数据造成影响。如果无法读源码就从“行为”上判断数字参数加单引号看是否报错字符串参数改变闭合看结果是否异常再结合时间延迟判断是否可以盲注。这里我不展开具体payload因为防御端更重要。测试的目的是发现问题而不是炫耀利用技巧。4.2 典型修复示例把拼接SQL改成参数化我处理过很多次类似的漏洞修复最典型的是后台登录接口和前台新闻详情页。下面给出一个可参考的改造过程。原始代码危险$id $_GET[id]; $sql SELECT title, content FROM news WHERE id $id; $result mysqli_query($conn, $sql);改造后安全$id $_GET[id]; $stmt $conn-prepare(SELECT title, content FROM news WHERE id ?); $stmt-bind_param(i, $id); $stmt-execute(); $result $stmt-get_result();这里用了bind_param(i, ...)明确告诉MySQL这个参数是整数。即使攻击者传入1 or 11也只会被当成字符串数字处理不会参与SQL逻辑。字符串类型同理使用bind_param(s, ...)PDO则统一用execute绑定。核心原则就一条任何外部输入都不能直接拼进SQL语句结构只能作为参数传递。除了参数化以下几类修复也常用对排序、字段名等不能参数化的值使用白名单校验对数据库账号做最小权限调整关闭secure_file_priv不必要的写入如果业务不需要保持NULL在Web层增加WAF规则但WAF只是缓解不能替代参数化上线前用自动化扫描器比如常见的漏扫工具做基础扫描再人工审计。4.3 常见问题速查表我在带新人或做项目复盘时经常整理下面的问题表遇到类似情况可以直接对照排查。现象可能原因处理方向万能密码对某个登录框无效代码用了参数化查询或对引号做了转义确认是否所有输入点都已参数化不要依赖黑名单联合查询结果只有一列原SQL查询字段数不匹配逐步调整字段数但测试时注意不要造成数据异常LOAD_FILE()返回NULL权限不足或secure_file_priv限制检查当前用户权限调整配置文件但生产环境不建议放开INTO OUTFILE写入失败目录不可写、文件已存在、权限不足确认导出目录限制如业务需要指定专用目录并控制权限跨库查询报错“拒绝访问”账号没有对应库权限业务账号只授权本库属正常防护不应为了功能放开PHP页面提示数据库连接数超限长连接或慢查询过多开启慢查询日志优化SQL避免全表扫描这张表看着简单但每条背后都有实际案例。比如“万能密码无效”很多人第一反应是“payload不对”其实最可能是代码已经更新为参数化查询漏洞早就被修复了。5. 踩坑心得与防御底线最后聊点我在实际项目中踩过坑之后总结出来的体会。第一个坑是“只关注注入不关注权限”。有一次做某管理系统的测试明明找到了一个明显的搜索注入但试了半天跨库查不到任何东西后来一查数据库账号才发现开发早就按规范只给了当前库的权限。这说明权限配置到位能极大削弱注入的破坏力。从那以后我每次做安全方案一定把“数据库权限最小化”列为整改项而不是只改代码。第二个坑是“过滤一时爽绕过火葬场”。早期我也尝试过用正则过滤关键词结果测试时被%、_等通配符和编码转换绕得头疼。后来彻底转向参数化查询业务代码也清爽很多。给PHP开发者的建议是不要在过滤和黑名单上花太多精力那是跟攻击者玩猜谜猜不赢的。老老实实预处理、白名单校验才是正道。第三个坑是文件读写。大家一听到“数据库写文件”就觉得能直接拿shell但实际上secure_file_priv、目录权限、MAC系统SIP、SELinux等一堆限制都会拦着。所以我现在的习惯是在做测试或加固时先查secure_file_priv的值再查Web目录的权限再考虑是否走文件读写这条路。不要一上来就浪费时间。根据我的个人经验安全这件事七分靠架构和配置两分靠代码质量一分才靠应急响应。PHPMySQL这套组合本身不背锅问题是“有没有在数据库连接这层做好隔离”。如果你正负责维护某个PHP项目不妨现在就做三件事第一改掉所有拼接SQL统一用预处理第二把数据库账号换成最小权限第三查一遍secure_file_priv和账号权限表把不需要的FILE权限全部收掉。做完这三步80%的SQL注入风险就已经被挡在门外了。