跟我零基础跟玩RSC反序列(3)

admin 2026-04-23 05:58:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文是RSC反序列化漏洞分析系列的第三篇,深入剖析了Next.js中基于busboy的多表单数据解析流程。文章从pipeline将请求体流入busboy开始,逐步追踪decodeReplyFromBusboy函数如何通过field和file事件回调处理分块表单数据,重点分析了createResponse初始化response对象的过程以及resolveField在每次表单字段解析时将数据存入FormData和chunksMap的机制,指出chunks初始为空导致无法进入resolveModelChunk分支,为后续漏洞利用链构造奠定代码审计基础。 综合评分: 77 文章分类: 代码审计,漏洞分析,WEB安全


cover_image

跟我零基础跟玩RSC反序列(3)

原创

YMsora YMsora

YMs0ra的安全漫路

2026年4月17日 23:18 浙江

在小说阅读器读本章

去阅读

if (isFetchAction) { // A fetch action with a multipart body. const busboy = require('next/dist/compiled/busboy')({ defParamCharset: 'utf8', headers: req.headers, limits: { fieldSize: bodySizeLimitBytes } }); // We need to use `pipeline(one, two)` instead of `one.pipe(two)` to propagate size limit errors correctly. pipeline(sizeLimitedBody, busboy, // Avoid unhandled errors from `pipeline()` by passing an empty completion callback. // We'll propagate the errors properly when consuming the stream. ()=>{}); boundActionArguments = await decodeReplyFromBusboy(busboy, serverModuleMap, { temporaryReferences });

前置我们已知busboy过decodeReplyFromBusboy解析后返回了boundActionArguments,body被pipeline进了busboy,这里busboy处理逻辑不做赘述。

因为

 pipeline(sizeLimitedBody, busboy, // Avoid unhandled errors from `pipeline()` by passing an empty completion callback. // We'll propagate the errors properly when consuming the stream. ()=>{});

所以参数busboy是作为流式数据源,serverModuleMap便是函数的映射表。

我们接着溯源到函数decodeReplyFromBusboy

exports.decodeReplyFromBusboy&nbsp;=&nbsp;function&nbsp;(&nbsp;busboyStream,&nbsp;webpackMap,&nbsp;options&nbsp;) {&nbsp;var&nbsp;response =&nbsp;createResponse(&nbsp;webpackMap,&nbsp;"",&nbsp;options ? options.temporaryReferences&nbsp;:&nbsp;void&nbsp;0&nbsp;),&nbsp;pendingFiles =&nbsp;0,&nbsp;queuedFields = [];&nbsp;busboyStream.on("field",&nbsp;function&nbsp;(name, value) {&nbsp;0&nbsp;< pendingFiles&nbsp;? queuedFields.push(name, value)&nbsp;:&nbsp;resolveField(response, name, value);&nbsp;});&nbsp;busboyStream.on("file",&nbsp;function&nbsp;(name, value, _ref2) {&nbsp;var&nbsp;filename = _ref2.filename,&nbsp;mimeType = _ref2.mimeType;&nbsp;if&nbsp;("base64"&nbsp;=== _ref2.encoding.toLowerCase())&nbsp;throw&nbsp;Error(&nbsp;"React doesn't accept base64 encoded file uploads because we don't expect form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it."&nbsp;);&nbsp;pendingFiles++;&nbsp;var&nbsp;JSCompiler_object_inline_chunks_251 = [];&nbsp;value.on("data",&nbsp;function&nbsp;(chunk) {&nbsp;JSCompiler_object_inline_chunks_251.push(chunk);&nbsp;});&nbsp;value.on("end",&nbsp;function&nbsp;() {&nbsp;var&nbsp;blob =&nbsp;new&nbsp;Blob(JSCompiler_object_inline_chunks_251, {&nbsp;type: mimeType&nbsp;});&nbsp;response._formData.append(name, blob, filename);&nbsp;pendingFiles--;&nbsp;if&nbsp;(0&nbsp;=== pendingFiles) {&nbsp;for&nbsp;(blob =&nbsp;0; blob < queuedFields.length; blob +=&nbsp;2)&nbsp;resolveField(&nbsp;response,&nbsp;queuedFields[blob],&nbsp;queuedFields[blob +&nbsp;1]&nbsp;);&nbsp;queuedFields.length&nbsp;=&nbsp;0;&nbsp;}&nbsp;});&nbsp;});&nbsp;busboyStream.on("finish",&nbsp;function&nbsp;() {&nbsp;close(response);&nbsp;});&nbsp;busboyStream.on("error",&nbsp;function&nbsp;(err) {&nbsp;reportGlobalError(response, err);&nbsp;});&nbsp;return&nbsp;getChunk(response,&nbsp;0);&nbsp;};

接收的busboystream便是busboy对象输出的数据流,webpackMap也就是映射,便是serverModuleMap,只是换了个名字。

接下来便是busboystream输出的分块表单,进行回调函数处理,并且设置了队列queuedFields

&nbsp;busboyStream.on("field",&nbsp;function&nbsp;(name, value) {&nbsp;0&nbsp;< pendingFiles&nbsp;? queuedFields.push(name, value)&nbsp;:&nbsp;resolveField(response, name, value);&nbsp;});

4是控制field字段的队列,下面都是针对data为file情况的解析。

目光拉回creatResponse

&nbsp;var&nbsp;response =&nbsp;createResponse(&nbsp;webpackMap,&nbsp;"",&nbsp;options ? options.temporaryReferences&nbsp;:&nbsp;void&nbsp;0&nbsp;),

溯源:

function&nbsp;createResponse(&nbsp;bundlerConfig,&nbsp;formFieldPrefix,&nbsp;temporaryReferences&nbsp;) {&nbsp;var&nbsp;backingFormData =&nbsp;3&nbsp;<&nbsp;arguments.length&nbsp;&&&nbsp;void&nbsp;0&nbsp;!==&nbsp;arguments[3]&nbsp;?&nbsp;arguments[3]&nbsp;:&nbsp;new&nbsp;FormData(),&nbsp;chunks =&nbsp;new&nbsp;Map();&nbsp;return&nbsp;{&nbsp;_bundlerConfig: bundlerConfig,&nbsp;_prefix: formFieldPrefix,&nbsp;_formData: backingFormData,&nbsp;_chunks: chunks,&nbsp;_closed: !1,&nbsp;_closedReason:&nbsp;null,&nbsp;_temporaryReferences: temporaryReferences&nbsp;};&nbsp;}

webpackMap作为bundlerconfig传入,校验之后创建new formdata()对象

变量在这里再次更新后return

bundlerconfig,也就是webpackmap=_bundlerconfig。 _chunks变为new Map()对象.

_prefix为空字符串。

最后在这些状态挂在response对象的情况下进行getchunk

return&nbsp;getChunk(response,&nbsp;0);

并且注意到挂载队列的处理函数是resolveField

溯源

function resolveField(response, key, value) {&nbsp;response._formData.append(key, value);&nbsp;var&nbsp;prefix&nbsp;=&nbsp;response._prefix;&nbsp;key.startsWith(prefix)&nbsp;&&&nbsp;((response&nbsp;=&nbsp;response._chunks),&nbsp;(key&nbsp;=&nbsp;+key.slice(prefix.length)),&nbsp;(prefix&nbsp;=&nbsp;response.get(key))&nbsp;&&&nbsp;resolveModelChunk(prefix, value, key));&nbsp;}

这里response已经是挂载成一个集函数map的对象了,并且prefix因为是空字符恒成立,并且将response挂载为chunks

这里的chunks为一个空的对象。上面的key就是name字段,而response便是多表单为解析的空map()对象

这里区分一下,createResponse行为相对是单次的,而resolveField是每次表单执行一次

阶段来说。代码块8的response是有formdata和map两个空对象的

每次都将key和value字段存入formdata。但是另外的chunk开始是空的,也就是取不到key。

也就进不了resolveModelChunk分支。

但是每次decodeReplyFromBusboy结束前都会进一次getchunk。

继续跟进吧


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:YMs0ra的安全漫路 YMsora YMsora《跟我零基础跟玩RSC反序列(3)》

评论:0   参与:  0