mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(sandbox): undefine the raw fetch host bridge before user code runs (#6761)
* fix(sandbox): undefine the raw fetch host bridge before user code runs * test(sandbox): scope the hardening assertions to each execution path * fix(sandbox): preserve the fetch global's property attributes
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*
|
||||
* Guards the isolate hardening contract in `isolated-vm-worker.cjs`: no raw
|
||||
* `ivm.Reference` host bridge may survive as an isolate global once user code
|
||||
* runs. Each bootstrap must capture its bridges in a closure and list their
|
||||
* global names in its own `undefined_globals`.
|
||||
*
|
||||
* The two execution paths harden independently, so every assertion is scoped to
|
||||
* one path's source slice — a bridge installed by `executeCode` but undefined
|
||||
* only by `executeTask` must still fail.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const WORKER_SOURCE = readFileSync(join(__dirname, 'isolated-vm-worker.cjs'), 'utf8')
|
||||
|
||||
const EXECUTE_CODE_MARKER = 'async function executeCode('
|
||||
const EXECUTE_TASK_MARKER = 'async function executeTask('
|
||||
|
||||
/**
|
||||
* Source of one execution path, from its function declaration to the start of
|
||||
* the next one (or end of file for the last path).
|
||||
*/
|
||||
function pathSource(marker: string, nextMarker?: string): string {
|
||||
const start = WORKER_SOURCE.indexOf(marker)
|
||||
expect(start, `${marker} not found — worker layout changed`).toBeGreaterThan(-1)
|
||||
const end = nextMarker ? WORKER_SOURCE.indexOf(nextMarker, start) : WORKER_SOURCE.length
|
||||
expect(end, `${nextMarker} not found — worker layout changed`).toBeGreaterThan(start)
|
||||
return WORKER_SOURCE.slice(start, end)
|
||||
}
|
||||
|
||||
const EXECUTION_PATHS = [
|
||||
{ name: 'executeCode', source: pathSource(EXECUTE_CODE_MARKER, EXECUTE_TASK_MARKER) },
|
||||
{ name: 'executeTask', source: pathSource(EXECUTE_TASK_MARKER) },
|
||||
]
|
||||
|
||||
/**
|
||||
* Globals bound to a raw `ivm.Reference`. The worker names these `__*Ref`;
|
||||
* `ivm.Callback` bridges (`__log`, `__textEncode`, …) are plain isolate
|
||||
* functions that expose no host handle and are deliberately not matched.
|
||||
*/
|
||||
function referenceBridges(source: string): string[] {
|
||||
return [...source.matchAll(/jail\.set\('(__\w+Ref)'/g)].map((match) => match[1])
|
||||
}
|
||||
|
||||
function hardeningList(source: string): string {
|
||||
const list = source.match(/const undefined_globals = \[[\s\S]*?\]/)
|
||||
expect(list, 'no undefined_globals hardening list in this execution path').not.toBeNull()
|
||||
return (list as RegExpMatchArray)[0]
|
||||
}
|
||||
|
||||
describe('isolated-vm worker hardening', () => {
|
||||
it.each(EXECUTION_PATHS)(
|
||||
'$name undefines every ivm.Reference bridge it installs',
|
||||
({ name, source }) => {
|
||||
const bridges = referenceBridges(source)
|
||||
expect(
|
||||
bridges.length,
|
||||
`${name} installs no __*Ref bridges — detection is stale`
|
||||
).toBeGreaterThan(0)
|
||||
|
||||
const list = hardeningList(source)
|
||||
for (const bridge of bridges) {
|
||||
expect(
|
||||
list.includes(`'${bridge}'`),
|
||||
`${name} sets ${bridge} as an isolate global but its hardening list omits it`
|
||||
).toBe(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each(EXECUTION_PATHS)('$name undefines the isolated-vm escape globals', ({ source }) => {
|
||||
const list = hardeningList(source)
|
||||
for (const name of ['Isolate', 'Context', 'Script', 'Reference', 'ExternalCopy']) {
|
||||
expect(list).toContain(`'${name}'`)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -310,8 +310,12 @@ async function executeCode(request, executionId) {
|
||||
info: (...args) => __log(...args),
|
||||
};
|
||||
|
||||
// Set up fetch function that uses the host's secure fetch
|
||||
async function fetch(url, options) {
|
||||
// Set up fetch function that uses the host's secure fetch. The raw
|
||||
// host bridge is captured in this closure so the hardening step below
|
||||
// can undefine the global without breaking fetch().
|
||||
(() => {
|
||||
const __fetch = globalThis.__fetchRef;
|
||||
const fetchImpl = async function fetch(url, options) {
|
||||
let optionsJson;
|
||||
if (options) {
|
||||
try {
|
||||
@@ -323,7 +327,7 @@ async function executeCode(request, executionId) {
|
||||
throw new Error('fetch options exceed maximum payload size');
|
||||
}
|
||||
}
|
||||
const resultJson = await __fetchRef.apply(undefined, [url, optionsJson], { result: { promise: true } });
|
||||
const resultJson = await __fetch.apply(undefined, [url, optionsJson], { result: { promise: true } });
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(resultJson);
|
||||
@@ -355,7 +359,16 @@ async function executeCode(request, executionId) {
|
||||
blob: async () => { throw new Error('blob() not supported in sandbox'); },
|
||||
arrayBuffer: async () => { throw new Error('arrayBuffer() not supported in sandbox'); },
|
||||
};
|
||||
}
|
||||
};
|
||||
// Same property attributes a top-level \`function fetch\` declaration
|
||||
// produced, so user code sees an unchanged global.
|
||||
Object.defineProperty(global, 'fetch', {
|
||||
value: fetchImpl,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: false
|
||||
});
|
||||
})();
|
||||
|
||||
const sim = (() => {
|
||||
const broker = __brokerRef;
|
||||
@@ -408,7 +421,7 @@ async function executeCode(request, executionId) {
|
||||
const undefined_globals = [
|
||||
'Isolate', 'Context', 'Script', 'Module', 'Callback', 'Reference',
|
||||
'ExternalCopy', 'process', 'require', 'module', 'exports', '__dirname', '__filename',
|
||||
'__brokerRef', '__broker', '__callSimBroker'
|
||||
'__fetchRef', '__brokerRef', '__broker', '__callSimBroker'
|
||||
];
|
||||
for (const name of undefined_globals) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user