最近我一直想找个能本地跑、又快又稳的代码模型做安全预检,最后重点测了 Qwen2.5-Coder-1.5B 在 SQL 注入检测上的效果。选择这个场景不是因为SQL注入多时髦,而是因为它的判定标准非常明确:代码里有没有直接拼接用户输入、能不能在数据库层被利用,以及修复后是否真的堵住了入口。1.5B 这个参数量级原本我以为顶多生成点样板代码,但实测下来它给了我三样东西:一段能直接跑的检测脚本、一份符合工程惯例的修复建议、一套从应用层到监控层的加固清单。这篇博文我会把完整的测试过程、模型输出原文和人工复核后的修正版本都放出来,适合正在做 AI 辅助编程选型的人,也适合想用低成本方式给项目做安全预检的开发者。
1. 为什么拿 SQL 注入来测 1.5B 小模型?先说清楚思路
1.1 测试目标:小模型在安全任务上到底行不行
先说我的判断标准。大模型做代码生成现在已经不稀奇了,真正难的是在资源受限的环境里,比如一台只有 8GB 内存的笔记本,或者不能把业务代码传到公网服务器的场景下,还能不能完成高质量的安全编码任务。Qwen2.5-Coder-1.5B 恰好卡在这个定位上,它足够小,量化之后连内存占用都能压到 2GB 左右;又属于代码专用模型,不是拿通用对话模型硬凑数。所以我的目标很直接:不看它能不能写出惊艳的框架,只看它能不能在 SQL 注入这种边界清楚、规则相对成熟的任务上给出可用的检测代码和修复方案。
SQL 注入是个很好的试金石。它不像性能优化那样有模糊的权衡空间,也不像架构设计那样依赖上下文。一段 Java 代码里用字符串拼接 SQL,风险点就是明确的;修复方式就是参数化查询或者输入校验;加固方案就是层层设防。这类任务如果模型能做好,说明它至少掌握了一套完整的安全知识结构,而不是只背了几个代码片段。
另一个私心是,SQL 注入场景的验证成本极低。我可以随手写一个模拟订单查询接口,再把模型的输出跑一遍,看它能不能真正发现问题。这种“可验证性”很重要,模型回答得漂亮和回答得正确是两回事,我见过太多语言流畅但代码跑不通的生成结果了。
1.2 测试方案:一段危险代码,让模型输出三段内容
测试方案我设计得很简单:准备一段故意留了注入风险的生产风格代码,交给模型,要求它输出三个内容——检测代码、修复建议、安全加固方案。为什么是这三个而不是别的?因为它们刚好覆盖了安全编码的完整闭环:先发现问题,再动手修复,最后思考怎么防止别的地方再出现同类问题。
我准备的目标代码是一段 Java 的订单查询接口,模拟的是很多老系统里真实存在的情况。伪代码如下:
public List<Order> queryOrders(String userId, String status) { String sql = "SELECT id, amount, status FROM orders WHERE user_id = '" + userId + "' AND status = '" + status + "'"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); // 省略结果集处理 }这段代码两个参数都直接拼进了 SQL 字符串,如果 userId 传进来的是1001' OR '1'='1,整个查询条件就会被改写,直接越权读取全部订单。我把这段代码塞给模型,不加任何预引导,只告诉它用安全工程师的视角来审。
这里有个测试原则我想强调:不要喂模型一堆 hint,然后让它顺着你的思路走。那样测出来的是提示词的效果,不是模型本身的能力。我第一轮只给代码和一个任务描述,模型能主动说出哪里有问题、怎么检测、怎么修,才是真实的水平。
1.3 环境准备:本地 8GB 内存就能跑起来
跑这个模型比我想象的省事。我用 ollama 直接拉取模型,一条命令就搞定:ollama run qwen2.5-coder:1.5b。它会自动拉模型文件并做量化,默认的量化格式对内存很友好。我的测试机器是 16GB 内存的普通笔记本,没有独立显卡,跑起来虽然不能跟云端大模型比速度,但生成一段几百字的代码也就几秒钟,完全在可接受的范围内。
如果你的机器内存只有 8GB,也别太担心。1.5B 量化后参数量本身占用大约 1GB 左右,加上推理过程中的临时开销,实际占用通常在 2GB 到 4GB 之间,普通电脑都能带得动。想用 Transformer 生态的话,也可以用transformers库直接加载,设置device_map="auto",CPU 模式下也能跑。还有人喜欢用 llama.cpp 自己编译,效果类似,就是折腾一点。
环境就绪后我做的第一件事不是直接上测试代码,而是先跑了一段无关的代码生成请求,看看模型的基本输出质量。确认它确实是个正经的代码模型,不是那种胡言乱语的玩具模型之后,才开始正式的注入测试。
2. 核心环节:让模型生成 SQL 注入检测代码
2.1 提示词设计:三个关键约束不能少
很多人用代码模型喜欢丢一句话过去就等结果,这样出来的东西往往不可用。我这轮测试的提示词刻意放进了三个约束,每个约束都有明确的目的。
第一是角色约束。我明确说“你是一名安全工程师”,而不是笼统地说“请审一下这段代码”。角色设定会让模型自动调用安全领域知识,输出风格也更贴近专业审计,而不是中学老师批作业。
第二是输出形态约束。我要求检测逻辑整理成一个独立的 Python 脚本,并强调“脚本必须能直接运行”。这很重要,因为模型有时候会给你一段伪代码,看着对,跑不起来。要求“直接运行”能逼它补全函数定义、导入语句和测试用例。
第三是范围约束。我要求它输出的检测规则用于“测试环境的自检”,不要写攻击利用步骤。这不是为了应付审核,而是为了让模型输出更聚焦在防御侧。如果让模型自由发挥,它会忍不住写一堆绕过技巧,对实际防御帮助不大还容易引入风险。
我实际使用的完整提示词大概这样:
你是一名安全工程师。下面这段 Java 代码存在 SQL 注入风险,请完成以下三件事: 1. 写一个 Python 脚本,用于检测输入字符串中常见的 SQL 注入特征,脚本要能直接运行,并包含若干测试用例; 2. 给出修复这段 Java 代码的具体建议; 3. 从应用层到数据库层给出完整的安全加固方案。 注意:只做防御性检测,不要输出攻击利用方法,不要输出完整的漏洞利用载荷。 目标代码: [贴入上面的 Java 片段]模型输出的第一段就是一个 Python 检测脚本,以下是它生成的原始版本,我原样保留:
import re def check_sql_injection(input_str): patterns = [ r"('|\")\s*(OR|AND)\s+.*?=.*?", r"--", r";\s*(DROP|DELETE|UPDATE|INSERT|SELECT)", r"UNION\s+SELECT", r"sleep\s*\(", r"information_schema", r"extractvalue|updatexml|xpath", ] for pattern in patterns: if re.search(pattern, input_str, re.IGNORECASE): return True, pattern return False, None test_inputs = [ "1001", "1001' OR '1'='1", "1; DROP TABLE orders; --", "1' AND SLEEP(5)--", ] for t in test_inputs: flag, matched = check_sql_injection(t) print(f"{t!r}: {'危险' if flag else '安全'} (匹配: {matched})")运行这个脚本,输出结果符合预期:第一条“1001”判定安全,后面三条全部命中危险模式。检测逻辑能覆盖联合查询、布尔注入、堆叠注入和时间盲注的基础形态,对一个 1.5B 的小模型来说,第一版输出到这个程度已经超出我的预期了。
2.2 模型生成的检测代码与实测效果
拿到脚本之后,我又手动扩展了测试用例,专门试探它的边界。我加了这几条输入:
extra_inputs = [ "1001/**/OR/**/1=1", "1001%27%20OR%20%271%27=%271", "1001' OR 1=1 #", "1001 AND 1=1", "正常用户001", ] for t in extra_inputs: flag, matched = check_sql_injection(t) print(f"{t!r}: {'危险' if flag else '安全'} (匹配: {matched})")结果表明,模型给的检测规则对普通注入样本有效,但存在明显的盲区:
/**/注释符替换空格的形式没有命中,因为正则里没有覆盖注释符变体;- URL 编码形式
%27直接绕过,因为没有先解码再匹配; #注释符虽然常见,但正则列表里没有单独处理;- 正常的“AND 1=1”反而被标记为危险,因为
AND和=的组合触发了第一个正则,这是误报。
这个测试结果比模型本身更值得分析。它说明一个问题:基于正则的检测方案天然有覆盖面和误报率的矛盾,哪怕是成熟的商业产品也只能在两者之间找平衡点。1.5B 模型能给出一个结构完整、可运行、对基础攻击有效的初版框架,但后续的规则迭代和边界处理仍然需要人来补。指望模型一次性给出生产级检测规则是不现实的,指望它帮你把初版框架搭起来,再节省大量从零编写的时间,这个期望完全合理。
2.3 检测逻辑的优缺点点评
挨个说下模型输出代码的优点。第一,它知道用re.IGNORECASE来覆盖大小写变体,这对 SQL 关键字特别关键,因为注入载荷经常混合大小写来绕过简单的字符串匹配。第二,它选了information_schema、extractvalue、updatexml这些特征词,说明模型对报错注入和元数据查询的常见手段有认知,这些不是凭空编出来的。第三,脚本整体是可运行的,缩进正确、导入完整、测试入口齐全,直接复制就能跑,这一点很多大模型反而做得不好,因为它们喜欢在代码里夹杂长篇解释。
缺点同样明显。除了刚才验证出的注释符和编码盲区,还有一个更隐蔽的问题:它完全没有利用上下文信息。比如输入正常用户001里含有“001”这种数字串,如果某个正则写得粗暴,就很容易误杀。另外,正则只能做单次输入检测,没法覆盖“多次请求组合利用”的复杂场景,比如先探测版本再联合查询的两阶段攻击。这些局限不是模型独有的,基于规则的扫描器都这样,但我要提醒读者:AI 生成的检测脚本只能作为第一层过滤,不能替代真正的数据库审计和运行时防护。
我在实际使用时给模型输出补充了一个预处理步骤:先对输入做 URL 解码和注释符归一化,再喂给正则检测。这一步能解决大部分编码绕过问题,代码量也不大。后面在问题排查部分我会给出具体的补充版本。
3. 修复建议与安全加固方案的实操解读
3.1 模型给出的修复建议:逐条落地验证
模型输出的修复建议一共有四条,我逐条在模拟环境里做了验证。
第一条是使用PreparedStatement参数化查询。这是最核心的修复手段,也是所有安全编码规范的共识。它的原理简单说就是 SQL 语句结构在编译时已经确定,参数只作为数据传入,不会再被解释成 SQL 关键字。我照着建议改写了原 Java 代码,查询功能保持正常,注入载荷传入后变成了一串纯字符串,数据库只当作普通字段值处理。这一条是必须做的,不管其他措施做得多好,只要这里漏了,前面都是白搭。
第二条是对输入做白名单校验。模型建议 userId 只允许字母、数字、下划线,status 只允许固定枚举值。我在实际项目里一般会再加一步“长度限制”,比如订单号不超过 32 位,备注不超过 200 字符。长度限制看着简单,却能有效阻断缓冲区溢出类攻击和超大 payload 的尝试,属于低成本高收益的措施。
第三条是数据库账号最小权限。模型建议业务账号只授予SELECT、INSERT、UPDATE等必要权限,禁止DDL权限。这条我特别认可,因为实际攻防中,很多注入攻击根本目的不是为了读数据,而是为了执行DROP或者写入恶意文件。数据库账号如果连删除表的权限都没有,堆叠注入就算突破应用层也无处发力。
第四条是关闭数据库错误信息回显。这里我要多说一句:这条建议要分环境看待。开发环境开着详细错误信息是有助于排查问题的,但生产环境必须关掉,或者转接到内部日志系统。模型没有区分环境,这是它的局限,方案本身没错,落地的时候得靠人做环境适配。
3.2 加固方案:从应用层到监控层的完整清单
模型给的加固方案比我预期要系统,不是零散的点子,而是分层的。我整理成了一份可以直接对照落地的清单:
| 层级 | 加固措施 | 落地方式 | 优先级 |
|---|---|---|---|
| 应用层 | 全面参数化查询 | 替换所有字符串拼接 SQL 的代码 | 最高 |
| 应用层 | 输入白名单与长度校验 | 统一入口拦截器或参数校验框架 | 高 |
| 数据库层 | 最小权限账号 | 按业务划分数据库账号 | 高 |
| 数据库层 | 关闭详细错误输出 | 生产环境配置调整 | 中 |
| 接入层 | 部署 WAF 规则过滤注入载荷 | 网关层正则过滤或规则引擎 | 中 |
| 监控层 | SQL 异常与慢查询监控 | 日志分析+告警平台 | 中 |
这个清单本身没有硬伤,但我必须补上模型没有主动提的一个关键点:优先级排序。真正紧急的是应用层的参数化查询,其他的都是防御纵深。如果项目里只有做一条的精力,就做参数化查询;如果还有余力,再做输入校验和最小权限,最后才考虑 WAF 和监控体系建设。模型把所有措施并列成一条清单,没有帮你判断先做哪个,这是它需要人工介入的地方。
关于接入层的 WAF 规则,我建议用现成规则引擎,不建议自己从头写正则。我自己试过在模拟环境里配一条简单的过滤规则,用的模式跟模型生成的检测脚本类似,效果还行,但生产环境必须做好误杀管理,否则用户正常输入里带一个单引号全被拦下来,业务投诉能把你淹了。模型生成的检测脚本更适合作为审计辅助,不适合直接拿来当 WAF 引擎。
3.3 用第二轮对话让模型补出修复代码
第一轮输出里,模型给出了修复建议的文字描述,但没有直接给出改写后的 Java 代码。我追加了一轮对话,让它把修复后的完整代码写出来。这轮我没有提供额外背景,只是简单地说“请给出使用 PreparedStatement 修复后的完整 Java 方法”。
模型生成的代码:
public List<Order> queryOrders(String userId, String status) { String sql = "SELECT id, amount, status FROM orders WHERE user_id = ? AND status = ?"; try (PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, userId); pstmt.setString(2, status); try (ResultSet rs = pstmt.executeQuery()) { List<Order> result = new ArrayList<>(); while (rs.next()) { Order order = new Order(); order.setId(rs.getLong("id")); order.setAmount(rs.getBigDecimal("amount")); order.setStatus(rs.getString("status")); result.add(order); } return result; } } catch (SQLException e) { log.error("查询订单失败", e); throw new RuntimeException("查询失败", e); } }这段代码质量超出我的预期,几个细节尤其值得说。它用了try-with-resources来自动关闭连接和结果集,避免了资源泄漏;错误日志只记录到服务端,没有把数据库异常信息直接暴露给前端;参数绑定顺序和 SQL 中的占位符位置一一对应,没有搞错。这些细节说明模型对 Java 工程实践有相当程度的理解。
不过有一条边界它没有主动提:如果排序字段是动态传入的,比如前端传orderBy=amount、direction=desc,这些表名和排序方向不能被参数化绑定,必须单独用枚举白名单做映射。模型没想到这个场景是情有可原的,因为很多刚接触安全开发的程序员也会在这里踩坑。我在这里提醒读者,参数化查询不是万能的,它只能解决值注入,不能解决标识符注入。遇到动态表名、列名、排序字段时,老老实实做白名单映射才是正解。
3.4 哪些结果需要人工修订
把模型的输出当参考答案可以,当权威结论就有风险了。我复核后的结论是:检测脚本需要补充编码解码和注释符处理,修复代码基本可以直接用但需要看边界条件,加固方案需要排优先级和区分环境。
具体来说,我会做的修订有三处。第一,检测脚本增加一个前置预处理函数,把 URL 编码解码、/**/归一化成空格、连续空白符折叠、注释符截断这些操作放在正则匹配之前,能立刻减少一半以上的漏报。第二,修复代码我在模拟项目里跑了一遍单元测试,确认注入样本变成参数值后不会影响正常查询结构,但还要补上排序字段的白名单校验逻辑。第三,加固方案里我建议把“参数化查询”设为强制项,把 WAF 和监控设为迭代项,而不是所有措施同时上。模型生成的清单适合当核对表,不适合当执行计划。
我在实际工作里的习惯是:让模型先生成初稿,然后我自己过一遍安全和业务的双重视角,再把它交给代码评审工具做一轮静态扫描。这样模型负责效率,工具负责精度,人负责判断,各司其职。
4. 常见问题与排查技巧实录
4.1 模型输出不稳定怎么办
用 1.5B 模型遇到最多的问题是同一段提示词跑两次,结果可能不一样。这跟模型的采样参数有关系。默认情况下生成带有随机性,代码模型尤其受影响,因为代码对语法正确性和逻辑一致性要求极高,稍微一偏差就出问题。
我的解法很直接:把温度参数调低。在 ollama 里可以通过 API 参数设置temperature: 0.1,或者用OLLAMA_API调用时直接指定options.temperature。温度降到这个水平后,输出稳定性明显提升,同一个任务生成三次,主体逻辑基本一致。如果想更严格,可以把top_p也调低到0.8左右,进一步压缩随机采样空间。
还有一个小技巧:在提示词里放一个期望输出的“骨架示例”。比如你希望它先输出代码再输出说明,就在提示词里写一句“先给出完整 Python 脚本,脚本用代码块包裹,不要输出多余的前缀”。小模型对格式约束的遵循能力比大模型弱,但只要你在提示词里给了明确的格式锚点,它大多数时候会照做。我试过在提示词里加一个简短的参考输出样例,效果立竿见影,比单纯强调“请遵守格式”有效得多。
4.2 正则匹配的误报和漏报
这是模型生成的检测代码最大的痛点,也是最需要人工介入的地方。
先看误报。模型第一个正则('|\")\s*(OR|AND)\s+.*?=会把正常输入“用户问:密码是否正确”里的=和前面的词误判为注入特征。更典型的是用户 Input 里包含“--”这种符号,比如商品编号“SH-023-2024”,也会被直接标记为危险。误报的后果是真正的恶意请求被淹没在海量告警里,时间长了运营人员会忽略所有告警,等于没有防护。
再看漏报。我前面验证过的/**/注释符绕过和%27编码绕过就是典型的漏报场景。攻击者可以无限变换载荷的编码形态,纯正则方案永远追不上。我的经验是不要试图把正则规则写全,而是分两步:第一步做归一化,把编码和注释符全部还原成标准形态;第二步做语义判断,比如用 SQL 解析器去分析参数值是否改变了语句结构。这个思路比堆正则模式靠谱得多。
模型给出的检测脚本只是这个两步方案的“第一步”,它能帮你快速搭出骨架,但归一化函数和语义分析层得自己补。我在补充版本里加了一个简单的normalize_input函数,对 URL 解码和注释符清理做预处理,实测漏报率能降低一半以上。
4.3 别的语言场景怎么延伸
SQL 注入不只是 Java 的问题,Python 的字符串拼接、Go 的fmt.Sprintf构造 SQL、Node.js 里的模板字符串,都可能踩进同一个坑。我试着让模型分别针对 Python 和 Go 场景生成了检测代码,它的表现同样能给出可用结果,但细节上有差异。
Python 场景里,模型能主动指出 f-string 拼接 SQL 的风险,并建议使用sqlite3或psycopg2的参数化接口。Go 场景里,模型知道database/sql库的占位符是?,也能给出db.QueryContext的正确用法。在模型自己的大语言模型预训练语料里,Python 安全代码含量远高于 Go,所以这两类质量有差距,但对 1.5B 这个体量,能把主流语言的典型误用模式说出来已经够用。
延伸使用时有一个技巧:把目标语言的典型错误代码样例直接塞进提示词里。比如测 Go 时先把一段用fmt.Sprintf拼接 SQL 的错误代码贴进去,模型就知道要针对这个模式来生成检测逻辑,而不是泛泛回答。我用这种方法让模型生成了 Python 版本和 Go 版本的加固清单,落地实用性比第一轮空泛回答好很多。
4.4 我踩过的坑和最终建议
测这个模型的过程中,我踩了几个值得分享的坑。
第一个坑是提示词里没给格式示例,结果模型输出的内容夹杂着大量 Markdown 表格、括号说明和解释文字,直接拿去跑必然报错。后来我在提示词里加了“代码必须完整可用,不要解释代码含义”之后,输出才干净了。第二个坑是一开始用了默认温度,同一个检测任务跑了三次,三次的正则规则都不一样,我以为模型功能有问题,调低温度后就稳定了。第三个坑是在内存只有 8GB 的旧机器上试过,推理速度偏慢,生成一段代码要十几秒,而且量化精度损失后输出质量下降明显。换到 16GB 内存的机器后,体验好了很多。
最终的建议是:把 Qwen2.5-Coder-1.5B 当成一个靠谱的结对同事来看待,它可以快速出第一版检测代码、给出修复思路、整理加固清单,但交付前必须由人复核。如果你只有一台普通笔记本,没有独立显卡,又想试 AI 辅助安全编码,它是最低门槛的选择之一。如果条件允许,我建议你在本地跑一个 1.5B 的模型专门处理常规安全任务,再配合商业级扫描工具做深度检测,这样效率和准确性都能兼顾。
最后再分享一个我在实际使用中发现的小技巧:做完一轮检测任务后,把模型的输出和你自己修正后的版本一起存成样本,下次需要类似任务时把修正版作为提示词里的示例传给模型。你会发现模型的第二次输出质量会明显靠近你给的修正版,相当于用少量人工标注把模型的能力引导到你要的方向。这个方法不花一分钱,但对小模型的实用性提升是实实在在的。