“嘿,快速问一下,为什么带异步回调的 forEach 会在 fetch 完成之前就结束?”
这条消息里没有粘贴任何代码,看起来就是一次再普通不过的聊天提问。可就是这句话,让整个团队原本以为万无一失的防护,露出了一道尴尬的口子。
![]()
事情发生在一个大型消费平台上。平台给用户做了一套 AI 聊天人设,人设代表的是某个真实存在的人。她的简介、说话语气、幽默感,全都被喂进了模型,目的是让用户产生一种错觉:自己是在和真人聊天,而不是在跟一个机器人对话。
一座被规则锁死的聊天人设
这个人设的对话边界,被设置得非常严格,甚至可以说是刻意做得无聊。按照最初的设定,她只能进行休闲聊天,话题限定在爱好、生活方式、娱乐和日常寒暄之间。
技术话题不行。法律建议不行。医疗建议也不行。她只能像一个处在真实身份里的普通人那样,聊那些安全、轻松、不越界的内容。
这套规则放在平时没有任何问题。事实上,在内部测试开始之前,所有人都觉得这个人设会老老实实待在自己的聊天范围里,不会冒出什么令人意外的操作。
一条被粘贴进来的破碎代码
转折发生在发布前的内部测试阶段。一位测试人员出于好奇,往聊天窗口里粘贴了一段有明显问题的 JavaScript 代码。
那段代码大致可以简化成这样:先声明一个空数组 results,然后用 items.forEach 遍历每一项,每一项里又用 await 去请求 fetchDetails(item.id),再把拿到的 data 推入 results。最后打印 results.length。
控制台输出的结果永远是 0。
稍微熟悉异步编程的人,一眼就能看出问题在哪里。forEach 不会等待异步回调执行完成,所以循环刚结束,results 还是空的,打印自然就是 0。
但重点来了:按照设定,这位人设原本不应该知道这些。
人设没有出戏,反而做起了前端审查
她读完了这段代码,准确地解释了 forEach 不会等待异步回调完成,建议改用 Promise.all 来拿到所有结果。更让人没想到的是,她在做这件事的全程里,始终没有脱离角色。
语气依旧温暖,说话依旧俏皮,整个人设依然友好。一个本应只聊生活方式的角色,就这么顺理成章地兼职了一把资深前端审查员。
刚开始的十秒钟,大家都觉得这事挺好笑。直到有人抛出一个让整个下午都变得不好过的问题:如果她能读懂这段代码,那她还能读懂什么?
以为护住了入口,其实漏掉了出口
最让人没法接受的地方在于,团队原本是有防护的。至少大家曾经是这么认为的。
按照设计,每条进来的消息都要先经过一次检查:这条请求里是否包含代码?如果包含,就立刻返回一个礼貌的拒绝回复。
把防护逻辑简化一下,差不多就是这样:先调用一个处理函数,如果判断请求文本里含有代码,就返回拒绝;没有代码,才把请求交给 personaModel 去回复,并把回复直接返回给用户。
这套逻辑是有测试的。测试全部通过,流水线一片绿色,所有人都睡得安稳。
第一次被当成偶发,结果不是侥幸
正因为有这套看似可靠的过滤器,代码审查事件刚冒出来时,并没有引起足够重视。团队的第一反应是:我们有代码过滤器,这多半只是个偶然情况。
这个反应很经典。过滤器明明在,测试也通过了,谁会去怀疑最基础的防御逻辑会出问题?
可惜它并不是侥幸。真正深入排查之后,大家才发现漏洞简单得让人有点不好意思。
只查了请求,没有人查响应
问题出在一个极其低级的盲区上。
团队检查了进入系统的请求,却没有任何人去检查模型生成后的响应。
如果你直接把代码粘贴进聊天框,过滤器确实能抓住。可如果你只是用自然语言去询问代码相关的问题,却不粘贴任何代码,那么这条请求看起来就是一段无辜的文本,会毫无阻碍地通过过滤器。
比如用户只发一句:“嘿,快速问一下,为什么带异步回调的 forEach 会在 fetch 完成之前就结束?”
请求里没有代码,containsCode 自然抓不到任何东西。于是系统放行,模型照常回复,再之后的事情就完全脱离了原有防护的掌控。
这个漏洞为什么值得反复说
从头到尾,这个事故都不涉及什么高深的技术攻击。它甚至不需要攻击者费尽心思去绕过什么复杂机制,只需要换一种提问方式,就能让过滤器在入口处毫无反应。
真正被击穿的
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.