去年有安全研究员在Twitter发截图——某知名电商搜索框输入一串单引号,整页用户密码直接蹦出。评论区有人问:不是用了ORM吗?怎么还会这样?

这个问题,我们用一段真实可复现的代码来回答。

打开网易新闻 查看精彩图片

这是一个专门学习Web安全的开源项目,技术栈是Next.js + Prisma。Prisma作为Node.js生态最受欢迎的ORM之一,确实能在大多数时候挡住SQL注入,但前提是你别主动绕过它的防护机制。而这个项目的搜索功能,恰恰踩了这颗雷。

实验环境:3分钟搭好靶场

想亲手复现漏洞,有两条路。本地Node.js环境:

npx create-oss-store oss-store

cd oss-store

npm run dev

或直接用Docker,连Node都不用装:

docker run -p 3000:3000 leogra/oss-oopssec-store

服务启动后访问 http://localhost:3000,你会看到功能完整的电商界面。商品列表、购物车、结账流程一应俱全。目标藏在最显眼的地方——顶部导航栏的搜索框。

攻击面分析:搜索框里的字符串拼接

搜索框调用的接口很直接:

/api/products/search?q=<搜索词>

用户输入什么,后端就搜什么。听起来正常,直到看一眼实现方式:后端收到q参数后,没有做任何转义或参数化处理,直接把字符串塞进SQL语句的LIKE子句。这意味着输入的每个字符——包括单引号、注释符、分号——都会原封不动变成SQL代码的一部分。

这种设计给攻击者留了敞开的门:用单引号闭合原本的查询上下文,后面就能接上任意SQL语句。典型的UNION注入场景。

第一步:确认注入点

先做正常搜索,输入fresh,能看到包含该关键词的商品列表,界面正常渲染,说明端点确实在用q参数查数据库。

现在换payload:

' UNION SELECT 1,2,3,4,5--

如果页面没报错,反而正常显示了一些"商品",注入成功。这个payload的工作原理是:前面单引号闭合原本的LIKE '%xxx%',后面UNION SELECT把5个常量合并进结果集,双横线注释掉原本语句的剩余部分。页面能渲染,说明后端完全信任了用户输入,把它当代码执行。

第二步:拖库实战

确认注入点后,下一步拿真实数据。需要知道users表有哪些列,以及它们的顺序。从代码里能看到,原始查询选了5列:id、name、description、price、imageUrl。所以UNION SELECT也要凑5列,类型可以混,但数量必须对齐。

最终payload:

DELIVERED' UNION SELECT id, email, password, role, addressId FROM users--

DELIVERED是商品名里的关键词,让前半句不返回空结果。后面的UNION把users表5个字段直接合并进商品查询结果。前端界面不会区分数据来自哪张表,只管渲染。于是你会在商品列表里看到一行行"商品"——名字是用户ID,描述是邮箱,价格是密码哈希。

用curl更直观:

curl "http://localhost:3000/api/products/search?q=DELIVERED%27%20UNION%20SELECT%20id%2C%20email%2C%20password%2C%20role%2C%20addressId%20FROM%20users--"

返回的JSON里,password字段躺着完整的用户凭证。

代码解剖:$queryRawUnsafe的危险承诺

漏洞核心在这里:

const sqlQuery = `

SELECT * FROM products

WHERE name LIKE '%${query}%'

OR description LIKE '%${query}%'

const products = await prisma.$queryRawUnsafe(sqlQuery);

注意那个Unsafe后缀。Prisma文档写得清楚:这个方法不转义、不参数化,把字符串原样发给数据库。开发者用了它,就等于亲手拆掉了ORM的防护墙。

更讽刺的是,Prisma提供了安全的替代方案——$queryRaw配合模板字符串标签函数:

const products = await prisma.$queryRaw`

SELECT * FROM products

WHERE name LIKE ${`%${query}%`}

同样能实现模糊搜索,但用户输入会被自动转义,注入攻击无从下手。区别只在于:有没有那个Unsafe后缀,以及有没有把变量直接嵌进模板字符串。

修复方案:一行代码的差距

最安全的改法是用参数化查询。如果业务需要动态表名或列名(本例不需要),可以配合查询构建器,但绝不要把用户输入直接拼接进SQL字符串。

另外,生产环境建议启用Prisma的查询日志审计,定期检查有没有代码调用了$queryRawUnsafe。这个函数名本身就是警示——除非万不得已,别碰它。

SQL注入在2024年依然稳居OWASP Top 10,不是因为技术多新,而是因为总有人图省事。ORM不是万能药,它提供的安全边界,需要开发者主动待在边界内。一旦为了"灵活"而绕过防护,代价可能是整库数据泄露。

那个Twitter截图里的电商,后来发公告说"已修复"。但没人知道,在修复之前,有多少人的密码已经被拖走。