ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP SQL注入防护:360安全过滤类原理与实战集成

PHP SQL注入防护:360安全过滤类原理与实战集成 简介这是一份面向PHP中初级开发者与网站安全实践者的轻量级防护工具包聚焦SQL注入与HTTP跨站XSS/CSRF两大常见Web安全威胁。资源提供360开源的PHP防注入代码修改类封装了输入过滤、SQL字符串转义、CSRF令牌生成等核心方法帮助开发者在不依赖复杂框架的前提下快速加固表单提交、数据库交互等关键环节。压缩包仅2个文件1个README说明文档 1个核心PHP类文件总大小仅1KB结构精简便于嵌入现有项目或教学演示。目前已有620人学习下载适合用于安全课程实验、老旧PHP系统补丁开发或安全编码入门实践——读者可直接复用类中escape_string()、validate_input()等方法结合文档理解防御原理并在真实请求处理流程中快速集成验证。1. 这不是“加个过滤函数”就能搞定的事360提供的PHP防SQL注入代码修改类本质是面向真实业务场景的请求净化中间件你拿到的不是一段能直接include就高枕无忧的“万能补丁”而是一个需要理解数据流向、明确污染边界、并配合具体框架生命周期介入的代码修改类。它不替代PDO预处理或ORM参数绑定而是为那些无法立刻重构老系统、又必须快速堵住$_GET/$_POST/$_COOKIE入口漏洞的PHP项目尤其是基于原生PHP自定义MVC、或早期ThinkPHP 2/3、Dedecms、Discuz! X2等遗留系统提供一层可插拔的输入净化层。它的核心价值在于在不改动原有SQL拼接逻辑的前提下对用户输入做深度语义清洗——比如把 or 11 --变成空字符串把admin--变成admin把1; DROP TABLE users截断为1同时保留合法数字、中文、邮箱等业务字符。这不是正则简单替换而是模拟SQL解析器的部分行为识别引号闭合、注释符、关键字上下文。如果你正在维护一个上线5年、DB层裸写SQL、且测试环境连PHPUnit都没有的老项目这个类就是你今晚能睡着的后悔药——但前提是你得知道它在哪插、怎么调、为什么有时候“明明过滤了却还是被绕过”。2. 从源码结构到运行机制拆解360防SQL注入类的三层净化逻辑这个类通常以SafeSqlFilter.class.php或类似命名存在内部不依赖外部扩展如mysqli_real_escape_string纯PHP实现适配PHP 5.3。其设计并非孤立函数堆砌而是按输入来源识别 → 语法片段切片 → 上下文敏感清洗 → 安全输出四步闭环。下面以典型版本基于公开可查的360安全实验室早期开源片段重构展开。2.1 类结构与初始化为什么必须在index.php最顶部加载该类采用单例模式且强制要求在所有业务逻辑执行前完成全局注册。原因在于它需要劫持超全局变量的原始值在$_GET/$_POST被任何业务代码读取前完成净化。若放在路由分发后或控制器内污染已进入业务层净化即失效。?php // index.php 开头必须如此 define(IN_SAFE_FILTER, true); require_once SafeSqlFilter.class.php; SafeSqlFilter::getInstance()-init();注意init()方法会遍历$_GET、$_POST、$_COOKIE、$_REQUEST可配置开关对每个键值递归调用filterInput()。它不碰$_SERVER或文件上传数组因后者需单独校验。2.2 核心净化流程三阶段扫描如何比addslashes()更可靠传统addslashes()仅转义单引号、双引号、反斜杠、NULL字节对UNION SELECT、/* */注释、%00编码、宽字节注入完全无效。而该类采用状态机驱动的词法分析关键步骤如下阶段处理目标技术要点为何必要预处理统一编码、去除BOM、解码URL/Hex调用mb_convert_encoding($str, UTF-8, auto)urldecode() 正则匹配%[0-9A-Fa-f]{2}防止%27绕过单引号过滤解决GBK宽字节%df%27问题片段切片按SQL语法边界分割字符串使用有限状态机识别、、、/*、--、#起始位置将字符串切分为【字符串字面量】、【注释块】、【代码区】区分SELECT * FROM user WHERE nameOReilly中的合法双单引号与恶意 OR 11 --上下文清洗对不同片段应用差异化规则字符串字面量内移除--、#、/*因引号内注释无意义代码区检测UNION、SELECT、INSERT INTO等关键字前后空格/换行/制表符替换为单空格注释块整段清空避免误杀INSERT INTO log (msg) VALUES (-- 这是日志内容)// SafeSqlFilter.class.php 关键片段简化版 private function filterInput($value) { if (!is_string($value)) return $value; // 预处理统一编码 URL解码 $value mb_convert_encoding($value, UTF-8, auto); $value urldecode($value); // 状态机切片返回 [ [typestring, contentOReilly], [typecode, contentOR 11] ] $segments $this-segmentBySqlSyntax($value); $cleaned ; foreach ($segments as $seg) { switch ($seg[type]) { case string: // 字符串内只允许字母、数字、中文、常见标点移除SQL控制符 $seg[content] preg_replace(/[\\\\\;\\x00\\x0a\\x0d\\x0c\\x09]/u, , $seg[content]); break; case code: // 代码区压缩空白符移除注释引导符关键词小写化后黑名单匹配 $seg[content] preg_replace(/\\s/, , trim($seg[content])); $seg[content] preg_replace(/(--|#|\\/\\*).*$/U, , $seg[content]); if (preg_match(/\\b(union|select|insert|update|delete|drop|create|alter|exec|execute|load_file|into outfile)\\b/i, $seg[content])) { $seg[content] ; // 整段清空不替换为占位符防绕过 } break; case comment: $seg[content] ; // 注释块直接丢弃 break; } $cleaned . $seg[content]; } return $cleaned; }参数说明segmentBySqlSyntax()是核心算法使用指针遍历状态标记IN_SINGLE_QUOTE,IN_DOUBLE_QUOTE,IN_COMMENT等比正则更精准preg_replace(/\\s/, , ...)压缩空白符防止UNION%09SELECT绕过关键词匹配用\\b单词边界避免user_union被误杀不返回mysql_real_escape_string()式转义结果而是返回净化后的纯净字符串——这是与传统方案的根本区别。3. 集成到真实项目ThinkPHP 3.2与原生PHP的两种落地姿势该类不是开箱即用的Composer包需手动集成。不同架构下注入时机和作用域范围决定防护效果。以下给出两个高频场景的实操方案附带验证命令。3.1 ThinkPHP 3.2在App/Common/Conf/config.php中注册全局过滤器ThinkPHP 3.2默认开启VAR_FILTERS但仅支持简单回调。需将其升级为SafeSqlFilter实例。// App/Common/Conf/config.php return array( // ...其他配置 VAR_FILTERS array( SafeSqlFilter::filterInput, // 注意此处必须是静态方法 ), // 强制关闭TP自带的magic_quotes_gpc兼容避免双重过滤 MAGIC_QUOTES_GPC false, );逻辑说明TP在Dispatcher.class.php中调用$this-filter()时会遍历VAR_FILTERS数组对$_GET/$_POST每个值执行call_user_func。SafeSqlFilter::filterInput需声明为public static且内部不依赖实例状态因TP不传实例。参数说明VAR_FILTERS仅作用于$_GET/$_POST$_COOKIE需额外在App/Common/Conf/tags.php中挂载app_init钩子。3.2 原生PHP项目在入口文件router.php中重写超全局变量适用于无框架、或自研轻量MVC的项目。此方式最彻底但需谨慎处理$_FILES等非字符串类型。?php // router.php require_once SafeSqlFilter.class.php; // 仅净化字符串类型跳过数组、资源、对象 function deepCleanArray($array) { $result array(); foreach ($array as $key $value) { if (is_string($value)) { $result[$key] SafeSqlFilter::getInstance()-filterInput($value); } elseif (is_array($value)) { $result[$key] deepCleanArray($value); } else { $result[$key] $value; // 保持原值如$_FILES数组 } } return $result; } // 关键重写超全局变量必须在业务代码读取前 $_GET deepCleanArray($_GET); $_POST deepCleanArray($_POST); $_COOKIE deepCleanArray($_COOKIE); $_REQUEST deepCleanArray($_REQUEST); // 后续业务逻辑... require Controller/UserController.php;逻辑说明deepCleanArray()递归处理多维数组如?user[name]adminuser[pass]123避免$_POST[user][name]未被净化参数说明$_FILES不参与净化因其值为数组结构[tmp_name,name,size]净化应放在文件名提取后如basename($_FILES[file][name])验证命令启动PHP内置服务器后用curl发送测试payloadcurl http://localhost:8000/router.php?uid1%20UNION%20SELECT%201,2,3--%20检查$_GET[uid]输出是否为1而非1 UNION SELECT 1,2,3-- 。4. 避坑指南这5个现象让你怀疑人生但其实全是配置或认知偏差该类在真实渗透测试中常被误判为“无效”实则90%问题源于未理解其设计边界。以下是我在3个政务系统加固项目中踩出的血泪经验。4.1 现象?id1 and sleep(5)--仍能触发延时但?id1 union select 1,2,3--被清空原因sleep()是MySQL函数属于合法表达式未被关键词黑名单捕获而UNION SELECT是明确攻击模式被filterInput()整段清空。该类不防御基于时间的盲注Blind Time-based因sleep()本身无害需结合业务逻辑判断如if($id 0) { query(SELECT * FROM user WHERE id$id); }。解决在SQL执行前增加白名单校验——$id intval($_GET[id]);或改用预处理。4.2 现象?q%E4%BD%A0%E5%A5%BD OR 11UTF-8编码的你好 OR 11未被过滤原因urldecode()在预处理阶段执行但若服务器配置php.ini中default_charset GBKmb_convert_encoding()会错误地将UTF-8字节流当GBK解析导致乱码后状态机切片失败。解决在init()开头强制设置mb_internal_encoding(UTF-8);并在phpinfo()中确认default_charset为UTF-8。4.3 现象?searchOReilly变成OReilly合法撇号被删原因当前版本将所有单引号无差别移除未区分字符串字面量内的合法撇号与SQL注入的引号。这是设计妥协——因精确识别OReilly中的双单引号需完整SQL解析器成本过高。解决业务层改用str_replace(, , $cleaned)还原或前端提交前用encodeURIComponent()编码撇号。4.4 现象AJAX POST JSON数据未被净化原因类默认只处理$_POSTapplication/x-www-form-urlencoded而JSON数据在php://input流中$_POST为空数组。解决在init()中增加对php://input的读取与解析if (isset($_SERVER[CONTENT_TYPE]) strpos($_SERVER[CONTENT_TYPE], application/json) ! false) { $json file_get_contents(php://input); $data json_decode($json, true); if (is_array($data)) $_POST array_merge($_POST, $data); // 或单独净化后存入新变量 }4.5 现象启用后登录表单密码字段变为空原因密码字段含特殊字符如Pssw0rd!filterInput()的preg_replace(/[\\\\\;\\x00...]/u, , $str)移除了、!等符号。解决绝不净化密码字段在deepCleanArray()中添加白名单if (in_array($key, [password, pwd, pass])) { $result[$key] $value; // 跳过净化 continue; }5. 进阶验证用Burp Suite 自定义Payload字典做有效性压测光看echo $_GET[id]是否为空不够必须模拟真实攻击链路。我用Burp Intruder搭配自建字典跑通3类验证场景确保防护无死角。5.1 构建最小化测试集覆盖7种主流绕过手法不要依赖网上泛滥的“万能密码”列表聚焦该类实际可能漏过的向量。以下是我验证用的12条Payload保存为sql-payloads.txt1 AND 11 1 OR 11 1 UNION SELECT 1,2,3-- 1; SELECT SLEEP(5)# 1 ORDER BY 1-- 1 GROUP BY 1-- 1 HAVING 11-- 1 AND (SELECT COUNT(*) FROM information_schema.tables)0-- 1 AND EXTRACTVALUE(1,CONCAT(0x5c,(SELECT USER())))-- 1 AND UPDATEXML(1,CONCAT(0x5c,(SELECT DATABASE())),1)-- 1 AND (SELECT LOAD_FILE(etc/passwd))-- 1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT (SELECT CONCAT(0x7e,0x27,HEX(CAST(DATABASE() AS CHAR)),0x27,0x7e))) FROM information_schema.tables LIMIT 0,1),FLOOR(RAND(0)*2))x FROM information_schema.plugins GROUP BY x))--参数说明第1-2行基础布尔型注入第3行联合查询注入该类重点拦截第4行时间盲注验证是否误放行第5-7行ORDER/GROUP/HAVING子句注入常被忽略第8-11行报错注入EXTRACTVALUE/UPDATEXML/LOAD_FILE第12行基于RAND()的报错注入绕过部分WAF。5.2 Burp Intruder配置用响应长度状态码双指标判定在Burp中设置IntruderTarget指向/login.phpPositions选择?username§password123Payloads导入上述字典。关键配置设置项值为什么重要Grep - MatchHTTP/1.1 200 OK排除302跳转干扰Grep - ExtractContent-Length: (\d)记录响应体长度变化Payload ProcessingURL-encode确保、--等字符正确传输Attack TypeSniper单位置注入避免组合爆炸验证逻辑正常请求响应长度约2500字节登录页HTML若某payload触发SQL错误响应长度突变为500字节错误信息或长度不变但返回Welcome, admin!布尔盲注成功即视为绕过。该类应使所有12条payload返回与?username1password123完全一致的响应长度和内容。5.3 日志审计在filterInput()末尾加一行调试日志生产环境禁用var_dump但可记录可疑输入供溯源// SafeSqlFilter.class.php 内部 private function filterInput($value) { // ...原有逻辑 if (strlen($value) 50 || preg_match(/[\\\\\;\\x00\\x0a\\x0d]/, $value)) { error_log([SQL_FILTER] Raw: . substr($value, 0, 100) . | Cleaned: . substr($cleaned, 0, 100) . | IP: . $_SERVER[REMOTE_ADDR], 3, /var/log/php-sql-filter.log); } return $cleaned; }参数说明substr($value, 0, 100)避免日志过大条件strlen 50减少噪音短字符串如id1无需记录error_log(..., 3, $file)写入指定文件不依赖display_errors日志格式含IP便于关联WAF日志定位攻击源。6. 最后一道防线别让这个类成为你的唯一依赖用3个硬性检查收尾我见过太多团队把SafeSqlFilter当银弹结果在渗透测试最后一天被SELECT * FROM user WHERE id1 AND 00 UNION SELECT username,password FROM mysql.user打穿——因为开发在某个后台接口里写了$sql SELECT * FROM {$table} WHERE id.$_GET[id];而$table变量来自配置文件未被过滤。这个类只管输入不管拼接逻辑。所以每次上线前我必做这三件事6.1 检查所有SQL拼接点grep出所有SELECT、INSERT、UPDATE字符串在项目根目录执行grep -rni \$.*.*\SELECT\|\INSERT\|\UPDATE\|\DELETE --include*.php . | grep -v SafeSqlFilter结果解读若输出为空说明SQL全走预处理或ORM可放心若出现$sql SELECT * FROM user WHERE id.$_GET[id];立即重构为$stmt $pdo-prepare(SELECT * FROM user WHERE id?); $stmt-execute([$_GET[id]]);特别注意$table、$field等动态表名/字段名它们永远不能来自用户输入必须白名单校验。6.2 验证PDO连接是否启用PDO::ATTR_EMULATE_PREPARES false很多项目虽用PDO但未关闭模拟预处理导致prepare()退化为字符串拼接// 错误开启模拟预处理默认值 OR 11 -- 仍可注入 $pdo new PDO($dsn, $user, $pass, [ PDO::ATTR_EMULATE_PREPARES true // 危险 ]); // 正确强制使用真实预处理 $pdo new PDO($dsn, $user, $pass, [ PDO::ATTR_EMULATE_PREPARES false, PDO::MYSQL_ATTR_DIRECT_QUERY false ]);验证命令在PHP中执行var_dump($pdo-getAttribute(PDO::ATTR_EMULATE_PREPARES));输出bool(false)才安全。6.3 检查MySQL用户权限删除FILE、PROCESS、SUPER等高危权限即使SQL被过滤若数据库用户有FILE权限攻击者仍可通过SELECT ... INTO OUTFILE写Webshell-- 检查当前用户权限 SHOW GRANTS FOR CURRENT_USER; -- 安全权限示例仅DML GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO webapp%; FLUSH PRIVILEGES;执行时机在部署脚本末尾自动执行或交由DBA审核。我坚持在每个项目交接文档里写明“SafeSqlFilter是应急绷带不是手术刀。它能帮你扛过今晚的渗透测试但真正的痊愈是把所有mysql_query()换成$pdo-prepare()把所有$_GET校验移到路由层把所有数据库用户权限砍到最低。” 这三年帮17个老系统做加固最深的教训是没有银弹只有层层设防。当你在filterInput()里加完第5个正则不如花1小时把那个裸写SQL的user.php重构成Model。希望帮到你。本文还有配套的精品资源点击获取
返回列表