首页 / 资讯中心 / 文章详情

Qwen2.5-Coder实战:本地部署1.5B模型批量修复PHP SQL注入

Qwen2.5-Coder实战:本地部署1.5B模型批量修复PHP SQL注入 ★ FEATURED ARTICLE
1. 效果展示的完整背景为什么要拿1.5B小模型做SQL注入修复先交代一下这个场景是怎么来的。我最近在维护一个老项目用的是第三方的PHP系统代码年代久远里面SQL查询基本都是字符串拼接。接手第一件事就是拿扫描器过了一遍结果不出意外注入点密密麻麻。本来想直接上商用修复方案但客户那边预算卡得死而且他们内部有一条规矩所有引入的工具和模型必须能本地部署数据不能出内网。商用API这条路直接被封死所以只能从开源模型里找方案。这时候我注意到Qwen2.5-Coder系列尤其是1.5B这个参数量。为什么选它三个原因一是体积小量化后不到2GB内网一台普通工作站就能跑二是代码生成能力在同等尺寸模型里确实能打尤其是针对PHP这类常见语言的基础缺陷修复三是它支持长上下文对完整函数级别的代码理解比之前的小模型强很多。我当时的想法是用一个大模型做漏洞检测和定位再用这个1.5B的小模型做定向修复分工协作成本和效果都能兼顾。这篇博文不打算讲那些高大上的理论就聚焦一件事拿一个真实的、包含SQL注入风险的PHP拼接查询代码跑一遍完整的修复流程把中间涉及的原理、模型的输出、人工的校正、以及最后如何验证安全效果一条线说清楚。看完之后你自己也能用这套流程处理类似的老代码。先强调一点1.5B模型不是一个全知全能的神器它有自己的边界。但在SQL注入修复这个具体任务上如果提示词给得清楚、修复骨架搭得对它的表现足够让人惊喜。后面我会把整个实现过程拆开讲包括我是怎么喂提示词的、模型返回了什么、我改了什么、以及最后为什么敢说这个修复是安全的。2. 问题代码的技术拆解SQL注入发生的三个必要条件2.1 漏洞代码的典型形态这次修复的目标代码是登录模块里的一段验证逻辑。代码大概长这样?php // user_login.php 片段 $username $_POST[username]; $password $_POST[password]; $sql SELECT id, username, role FROM users WHERE username . $username . AND password . md5($password) . ; $result mysqli_query($conn, $sql); if (mysqli_num_rows($result) 0) { // 登录成功跳转 } else { // 登录失败 }这段代码的问题一眼就能看出来直接把用户输入拼进了SQL字符串。但我在实际工作中发现很多人只知道这样有风险却说不清楚风险究竟出在哪。咱们把条件拆开看SQL注入能成立通常需要同时满足三件事第一用户输入进入SQL语句的方式是字符串拼接而不是参数绑定或转义。这条代码完美命中$username被直接拼进单引号包围的字符串里。第二输入在拼入之前没有经过任何过滤或白名单校验。这里甚至没有htmlspecialchars或mysqli_real_escape_string更别提参数化查询了。第三数据库连接使用了拼接后的整串SQL去执行且执行结果直接决定逻辑分支。mysqli_query拿到的是拼好的完整SQL注入的payload会作为SQL代码被数据库解析。现场验证一下假设用户名输入admin OR 11密码随便填一个拼出来的SQL就变成了SELECT id, username, role FROM users WHERE username admin OR 11 AND password xxxx由于OR的优先级关系11恒为真整个WHERE条件的真假就被绕过了。更严重的是如果数据库用户权限足够还可以用UNION SELECT把其他表的数据拖出来或者用INTO OUTFILE写文件。这些都不是危言耸听是我在渗透测试里真实见到过的操作。2.2 为什么老代码里这种写法这么普遍有人可能会问都什么年代了怎么还在用PHP拼接SQL这背后有历史包袱。早期PHP教程为了容易理解几乎全部使用字符串拼接来讲解数据库查询导致很多一线开发者形成的习惯就是“拼字符串”。还有一部分老系统是从更早的ASP或JSP迁移过来的迁移时只改了语法并没有顺手把所有查询重构成PDO参数化。再加上老项目通常没有自动化测试和代码审查流程这些漏洞就在代码库里一代代传了下来。我在这个项目里扫了一遍类似的问题能找出的拼接查询不下20处登录、列表筛选、后台搜索、导出功能里全都有。这也是为什么我觉得用模型批量修复比纯手工高效模型能快速产出基础修复版本人工只需要重点检查逻辑变动和安全边界。2.3 修复方向的选择预处理语句是主流方案针对上面的漏洞修复方案其实不止一种。最彻底的是把查询改成PDO的预处理语句也就是参数绑定。这种方案从机制上杜绝了注入因为查询结构在绑定参数之前就已经提交给数据库预编译了参数只会被当作数据处理不会被当作SQL指令解析。另一种方案是对输入做严格的转义比如使用mysqli_real_escape_string。这比字符串拼接安全一些但依赖转义函数是否覆盖所有字符集和场景。我以前遇到过因为数据库连接字符集设置不对导致转义在GBK宽字节场景下失效的案例所以不推荐当作首选方案。最终我决定引导模型朝着PDO预处理语句的方向修复。原因很简单这是当前PHP生态里最稳健、可维护性最高的方案而且能让模型的输出“有模板可依”生成质量会更稳定。如果让模型自己天马行空地选方案可能一次性给你换成存储过程或者ORM改完反而难以直接落地。3. 模型选型与本地部署把Qwen2.5-Coder-1.5B跑起来3.1 为什么不用更大参数的模型之前在实验室环境里我试过用7B和14B的代码模型来做修复任务。效果好是确实好但资源占用也摆在那里。特别是要给客户交付内网部署方案时对方的机器往往只是普通的8核16G服务器没有独立GPU。14B的量化模型在CPU上跑一次推理能等上几十秒批量修复20处代码光生成时间就是十几分钟而且并发一多内存直接吃紧。1.5B的模型在CPU上的表现就从容很多了。即使不开启GPU加速单条修复任务的生成时间可以控制在几秒到十几秒之间。一旦有GPU哪怕是几年前的旧卡速度也能提升到“秒出”。在这个任务里我们不做复杂的重写或者跨文件重构只是针对局部代码做安全加固1.5B模型的理解能力已经完全够用。3.2 部署环境的硬件配置与模型量化我在本地搭的部署环境是这样的一台配有16GB内存和4GB显存的工作站操作系统为Ubuntu 22.04Python环境为3.10版本安装了最新版的transformers库与torch。模型选的是Qwen/Qwen2.5-Coder-1.5B-Instruct为了减少内存占用我开启了4比特量化加载。量化加载在transformers里的写法很简单from transformers import AutoModelForCausalLM, AutoTokenizer model_name Qwen/Qwen2.5-Coder-1.5B-Instruct tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( model_name, trust_remote_codeTrue, load_in_4bitTrue, device_mapauto )这里有个细节load_in_4bitTrue需要配套安装bitsandbytes库。如果显存实在吃紧还可以换成load_in_8bitTrue虽然体积变大但精度损失更小。对于这种安全修复场景我更推荐4比特量化因为生成结果不会因为量化精度太低而出现格式崩坏的情况。3.3 推理服务的封装为了让后续的批量修复操作更顺滑我没有直接在脚本里反复调用裸模型而是封装了一个简单的推理函数。这个函数接收提示词返回模型的生成结果同时控制最大生成长度和温度参数。温度参数在这类修复任务中很关键我建议不要用默认的1.0而是设置在0.2到0.4之间。因为修复任务需要确定性输出过高的温度会让模型自由发挥容易把逻辑写歪。def generate_fix(prompt: str) - str: messages [ {role: system, content: 你是一名资深的PHP安全工程师擅长修复代码中的安全漏洞尤其是SQL注入问题。}, {role: user, content: prompt} ] text tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) inputs tokenizer([text], return_tensorspt).to(model.device) outputs model.generate( **inputs, max_new_tokens1024, temperature0.3, top_p0.9, do_sampleFalse ) response outputs[0][inputs.input_ids.shape[-1]:] return tokenizer.decode(response, skip_special_tokensTrue)do_sampleFalse这个参数值值得解释一下它意味着模型在生成时不再做随机采样而是每次都选择概率最高的token这就进一步压缩了随机性。对于修复代码这种“必须稳定复现”的任务关闭采样是正确选择。如果是在做代码注释生成或者风格转换这类需要多样性的任务才需要把do_sample打开并调高温度。部署好之后我还写了一个简单的自动化脚本把全部20处可疑代码直接提取出来逐个喂给模型输出再回写到临时目录。这一步能省去大量手工复制粘贴我会在后面的章节里展示完整流程。4. 提示词模板设计引导模型正确修复的关键4.1 提示词模板的原则给约束而不是给答案用模型修代码有一个很容易踩的坑以为把漏洞代码丢进去模型就会自动给出完美答案。实际上如果提示词不够明确模型的输出会五花八门有的只给解释不给代码有的给多种方案让你自己选还有的直接把整个文件重写一遍。这些都不是我们想要的。我这里的做法是把修复任务拆成三步全部写进提示词第一步标明漏洞类型和修复要求。明确说这是SQL注入漏洞必须使用PDO预处理语句修复。第二步给出原始代码片段。让模型看清上下文包括函数名、变量含义以及数据流向。第三步规定输出格式。要求只输出修复后的完整代码块不允许解释禁止修改原有函数逻辑只在SQL查询部分做最小改动。按照这个思路设计的提示词模板如下请修复下面这段PHP代码中的SQL注入漏洞。 要求 1. 使用PDO预处理语句替代原来的字符串拼接查询。 2. 保持原有业务逻辑完全不变包括参数名、变量名、函数名。 3. 只修改与SQL查询相关的部分不重构其他代码。 4. 输出完整修复后的代码块不要附带任何解释文字。 原始代码 php ?php $username $_POST[username]; $password $_POST[password]; $sql SELECT id, username, role FROM users WHERE username . $username . AND password . md5($password) . ; $result mysqli_query($conn, $sql); if (mysqli_num_rows($result) 0) { // 登录成功 } else { // 登录失败 } ?注意提示词里我没有生硬地规定PDO连接写法比如要不要new一个PDO对象。这是因为原代码里用的是mysqli系列函数直接切换成PDO类会涉及连接对象的创建和异常处理需要先了解数据库连接的初始化方式。如果我在提示词里硬塞一个假设的$pdo对象模型生成的内容很可能和实际项目不匹配。 更好的做法是给出一个后置约束如果原代码中没有定义PDO对象则先使用原mysqli连接转换创建。实际测试后我发现直接把模型输出结果拿来改往往还要手工补几行连接代码。所以我调整了策略在提示词里加上一句“可以将原有$conn对象传给PDO构造器但不要重复创建数据库连接。” ### 4.2 模型输出的原始代码与我的点评 直接把上面的提示词发出去模型生成的结果如下 php ?php $username $_POST[username]; $password $_POST[password]; try { $pdo new PDO(mysql:host . $conn-host . ;dbname . $conn-database, $conn-user, $conn-pass); $pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $sql SELECT id, username, role FROM users WHERE username :username AND password :password; $stmt $pdo-prepare($sql); $stmt-execute([ :username $username, :password md5($password) ]); $result $stmt-fetch(); if ($result) { // 登录成功 } else { // 登录失败 } } catch (PDOException $e) { // 记录日志并提示用户 exit(系统错误请稍后重试。); } ?客观说这个输出效果超出我的预期模型不仅正确地把查询改成了预处理语句还额外做了三件好事一是增加了try...catch异常处理避免数据库连接失败时暴露敏感信息二是用$conn-host这类属性提取原来的连接参数说明模型理解了原代码上下文三是把mysqli_num_rows($result) 0判断改成了$stmt-fetch()直接判断这在逻辑上等价而且更符合PDO习惯。但这份代码不能直接上生产环境有三个问题必须人工修正第一个问题$conn是mysqli对象它并没有host、database、user、pass这些公开属性。有些版本可能能访问到但这不是标准行为。需要回到项目里查一下数据库连接的初始化代码把真正的连接参数找出来或者直接复用原来的连接信息构造PDO对象。第二个问题原代码里使用md5($password)做密码哈希这个本身就不安全但这次任务只修SQL注入我不打算让模型顺手改密码存储方案。因为一旦改了会牵扯到旧密码校验逻辑和用户无感知迁移属于另一个独立的安全事项硬塞进来会让变动范围失控。第三个问题异常处理中直接exit(系统错误)虽然安全但不够友好。建议根据项目现状改成日志记录加页面临时提示或者对接到现有的错误处理机制中。4.3 人工修正后的最终代码我把模型输出中和实际项目不匹配的地方修正后最终落地到生产环境的代码如下?php // 在项目全局初始化处已定义 $dbConfig 数组 $username $_POST[username]; $password $_POST[password]; try { $pdo new PDO( mysql:host{$dbConfig[host]};dbname{$dbConfig[dbname]}, $dbConfig[user], $dbConfig[pass], [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION] ); $sql SELECT id, username, role FROM users WHERE username :username AND password :password; $stmt $pdo-prepare($sql); $stmt-execute([ :username $username, :password md5($password) ]); $user $stmt-fetch(); if ($user) { // 登录成功按原逻辑继续 } else { // 登录失败按原逻辑继续 } } catch (PDOException $e) { error_log(登录查询失败: . $e-getMessage()); // 页面对用户提示不暴露具体错误 }这样改完SQL注入的路子就堵死了。因为数据库在执行查询之前先通过prepare解析了SQL结构后续传进去的:username和:password只是纯粹的参数值即使包含单引号、UNION、注释符号等特殊字符也只会被当作字符串字面量处理不可能改变查询结构。5. 批量修复的完整流程从代码扫描到自动化改写5.1 第一步用正则与分词工具圈定可疑代码手工从项目里翻出所有拼接SQL太费时我写了一个Python脚本先扫描目录下所有PHP文件利用正则表达式匹配包含$sql赋值且存在字符串拼接和$_POST、$_GET等超全局变量的代码段。正则不能做得太复杂否则会有误报漏报。我的简化版规则是匹配以下特征包含SELECT、INSERT、UPDATE、DELETE关键字的字符串赋值。赋值表达式中出现. $或者.$这种连接符号。同一函数或代码块中出现了$_POST、$_GET、$_REQUEST。这三个特征同时命中时基本可以判定为拼接查询。脚本会把命中的代码段提取出来保留行号和上下文输出为一个待修复清单。这一步大概跑了不到一分钟就找出了20多处可疑点。5.2 第二步分批喂给模型并自动对比差异拿到清单后我写了一个批量处理循环。每次从清单里取一个代码段套用前面设计的提示词模板调用本地模型生成修复结果然后把修复前后的代码存入一个表格连同修改说明一起输出。这里有一个关键设置调用模型时使用temperature0.3和do_sampleFalse保证同一段代码即使跑多次生成结果也基本一致便于人工审核。批量处理的伪代码如下import re import json def extract_suspicious_php(file_path): with open(file_path, r, encodingutf-8) as f: content f.read() # 简化处理按分号粗划分代码块 blocks re.split(r(?;)\s*, content) suspicious [] for idx, block in enumerate(blocks): if ($sql in block or query( in block) and ( . $ in block or .$ in block ) and any(k in block for k in [$_POST, $_GET, $_REQUEST]): suspicious.append((idx, block)) return suspicious def batch_fix(file_path): suspicious extract_suspicious_php(file_path) results [] for idx, block in suspicious: prompt build_prompt(block) fixed generate_fix(prompt) results.append({ file: file_path, block_idx: idx, before: block, after: fixed }) return results实际运行中模型对大多数简单拼接查询的处理都非常到位。比如列表查询里的WHERE status {$status}直接就被改成了:status的占位符。但我绝不建议直接信任模型输出直接覆盖代码必须有人工审核环节。5.3 第三步人工审核的重点清单批量修复后我根据长期维护经验总结了一份人工审核清单每一条都是踩过的坑检查变量名是否被模型意外改动。有的输入字段叫$userId模型可能会习惯性改成$user_id这在PHP里可就是两个完全不同的变量了。检查是否有未定义的常量或函数。模型偶尔会凭空生成一个$db-prepare()但原项目里根本没有$db这个对象。检查关键业务逻辑是否被等价替换。比如mysqli_num_rows($result) 0和$stmt-fetch()在多数场景下等价但如果有while循环遍历多条记录就绝对不能改成fetch()必须用fetchAll()。检查新增的异常处理是否会吞掉原有逻辑。有些老代码在查询失败后还依赖die()或自定义错误页面改成try...catch后需要确保错误路径一致。检查是否引入不兼容的PHP语法。PDO对象是PHP 5.1.0之后引入的如果对方还在使用远古版本的PHP那就要考虑换成mysqli_stmt预处理方式。针对这次登录模块的修复我在审核时重点确认了$user $stmt-fetch();这个改动是否影响登录后逻辑对结果集的处理。看完上下文发现原逻辑只是判空所以完全没问题。5.4 批量修复后的效果对比修复完成后我拿原始代码和修复后的代码分别做了一次安全验证。构造一组典型的注入payload比如admin OR 11 x UNION SELECT username, password, role FROM users-- admin#在原始代码上这些payload全部能绕过验证拿到登录权限。而在修复后的代码上这些payload只被当作普通字符串去匹配数据库中的记录自然找不到任何用户返回登录失败。从安全和功能两个维度看修复结果都符合预期。6. 安全验证与回归测试如何证明修复真的有效6.1 搭建本地验证环境因为不能直接在客户生产环境做测试我在本地搭了套一模一样的PHP环境版本、数据库字符集、数据表结构都从生产环境复制过来。数据库里准备了一些测试账号其中有一个叫admin的正式账户还有几个带特殊字符的用户名比如testuser用来验证修复后能否正常登录。验证分两层第一层是注入攻击测试第二层是正常功能回归测试。只有两者都通过才敢说这次修复是可靠的。6.2 攻击测试脚本我写了一个简单的Python脚本模拟HTTP请求往登录接口发送不同的payload。因为测试环境通过本地端口访问不涉及外部网络速度很快。import requests payloads [ admin OR 11, admin -- , UNION SELECT username,password,role FROM users--, admin#, OR 11 LIMIT 1--, ] url http://127.0.0.1:8080/user_login.php for p in payloads: data {username: p, password: whatever} resp requests.post(url, datadata, allow_redirectsFalse) if resp.status_code 302 or 登录成功 in resp.text: print(f危险payload {p} 绕过了登录) else: print(f安全payload {p} 被拦截)运行结果中所有payload均未触发登录成功说明预处理语句在防止注入方面是有效的。这里值得注意的一点是即使数据库里不存在这种特殊字符的用户payload在SQL层面也不会被解析因为查询结构是死的参数只是数据流的一部分。6.3 回归测试攻击测试通过之后还要确保正常账号能正常登录。我从测试库中选取了三个不同状态的账号启用状态的正常用户、禁用状态的用户、用户名中带单引号的用户。特别添加带单引号的用户是因为以前遇到过一些转义方案把单引号转义后导致用户名匹配不上的问题。使用预处理语句后单引号作为普通字符储存和比对完全不需要额外处理。三个账号的登录结果用例预期结果实际结果正常启用账号登录成功跳转登录成功禁用状态账号登录失败提示无权限返回原逻辑的失败分支带单引号用户名账号登录成功与数据库中记录匹配登录成功回归测试全部通过说明改动没有影响原有身份认证流程。6.4 对比验证同样的payload在修复前的表现为了把效果展示得更直观我在本地保留了一份未修复的代码副本用同样的脚本去测试。那一边的测试结果基本就是全军覆没所有payload都能成功登录。这个对比让我确信模型生成的修复代码不是“表面合规”而是真正切断了注入路径。在实际的安全评审过程中我还会把修复后的代码再交给漏洞扫描器跑一遍。扫描器之前对拼接查询会报告高危漏洞修复后重新扫描高危项降为零只剩一些中低风险的告警比如密码使用MD5存储。这类问题不在本次修复范围内但会单独作为后续治理建议提交给客户。7. 常见问题与排查技巧实录7.1 模型在修复时改变了缩进和注释风格本地模型不是格式化工具它生成代码时会带上自己惯用的缩进和注释风格。如果直接把生成内容覆盖到原文件diff会变得巨大混进大量无关改动不利于代码审查。我习惯在修复后做一次统一格式化保持项目的原有编码风格。操作方式是生成时只提取关键的预处理语句部分手动粘贴到原函数这样既保留了改动的最小化又保证了代码可读性。7.2 PDO连接复用问题原项目使用mysqli连接在新代码里新建PDO对象后会同时维持两个数据库连接造成资源浪费。我最终的方案是在项目初始化处增加一个get_pdo()函数返回共享的PDO单例其他修复处都通过这个函数获取PDO对象。这样既不影响原mysqli兼容层代码又能逐步过渡到PDO体系。7.3 模型把布尔判断误改成了fetchAll前面提到过模型在生成返回语句时对于预期只返回一条记录的查询倾向于使用fetch()。但如果原查询逻辑预期返回多行比如一个历史订单列表错误的改动会导致只能取到第一条数据。这里的人工审核一定要看查询条件里的主键或唯一索引。如果查询条件不是唯一定位就不要采纳fetch()方案改用fetchAll()并循环遍历。7.4 对字符集的处理PDO预处理虽然避免了注入但字符集设置错误依然可能造成乱码。在拼接mysql:host...;dbname...时务必加上charsetutf8mb4。我加入这个参数的方式是在PDO构造的DSN字符串中直接追加。这样能避免一部分由于字符集不一致导致的特殊字符存储或比较异常尤其适合中文项目。7.5 模型的输入过长导致生成截断如果一次喂给模型的代码超过其上下文窗口输出会被截断出现残缺语句。解决方法是把原函数精简成最小可复现片段只保留SQL相关的变量和查询语句把无关的HTML输出、日志代码去掉。这跟给人类看代码一个道理信息太多反而干扰重点。7.6 批量修复后的全量SQL审计最后提醒一下找模型的间接手段不能取代人工代码审计。模型能修复注入但第三方扫描器给出的漏洞清单可能不够全面。我在这次项目里除了让模型修明显漏洞之外还专门跑了一遍全量SQL语句审计把所有查询统一列出来逐个确认是否都使用了预处理或参数化。这一遍虽然慢但能堵住那些模型没发现、漏网的拼接点。8. 这次实战之后的几点体会这一套流程跑下来我最大的感受是1.5B的模型在代码安全修复这个垂直任务上实用性被低估了。很多人一谈代码大模型就只盯着7B、14B、32B这些大参数但在资源受限的内网生产环境1.5B模型配合精心设计的提示词已经能解决很大一部分真实问题。它不需要昂贵的显卡不需要云服务交付也完全可控。说句实在话小型代码模型真正的短板不在理解力而在指令遵循的稳定性。同一个提示词跑多次可能有几次格式不对、逻辑不顺。所以使用它的正确姿势不是全自动无人干预而是把它当成一个不知疲倦的“初级工程师”一次性给你生成20个修复初稿再由资深工程师快速审核收尾。这个配合模式在效率上远超纯手工修可靠性上又胜过纯自动覆盖。如果你手头也有老PHP项目需要清理SQL注入完全可以按照我上面这一套流程跑一遍先扫描圈定范围再搭一个本地Quantized模型写清楚提示词批量生成修复初稿最后人工审核加攻击回归测试。过程中最大的坑其实是“图快直接覆盖原文件”只要守住审核这一关模型能帮你省下80%的重复劳动。最后再分享一个细节我每次修复完都会把模型生成的代码和最终人工修改后的代码保存下来做一次三路对比记录模型哪里做得好、哪里需要补正。积累几次之后提示词就会越来越准命中率明显上升。这种“人机协作的自我进化”才是小模型落地时真正值得投入时间打磨的地方。
阅读完成 · 觉得有帮助?
咨询建站