MCPcopy
hub / github.com/patriksimek/vm2 / sanitizeThenableArg

Function sanitizeThenableArg

lib/setup-sandbox.js:1331–1441  ·  view source on GitHub ↗
(value)

Source from the content-addressed store, hash-verified

1329 }
1330
1331 function sanitizeThenableArg(value) {
1332 if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return value;
1333 // ALWAYS wrap. Do not pre-read value.then — a getter could behave
1334 // differently on the second call and bypass the wrap (sub-attack 3).
1335 return {
1336 then: function safeThen(resolve, reject) {
1337 // Read user.then exactly once; V8 will not re-read because it
1338 // already captured `safeThen` (a fixed function) at PromiseResolve
1339 // time and uses that captured ref for the resolver job.
1340 let userThen;
1341 try {
1342 userThen = value.then;
1343 } catch (e) {
1344 if (typeof reject === 'function') {
1345 try {
1346 reject(safeSanitize(e));
1347 } catch (rejectEx) {
1348 /* best effort */
1349 }
1350 return undefined;
1351 }
1352 throw safeSanitize(e);
1353 }
1354 if (typeof userThen !== 'function') {
1355 // SECURITY (v7 — GHSA-248r-7h7q-cr24): the v6 fix tried
1356 // to preserve identity for benign non-thenable inputs by
1357 // passing `value` to `resolve()` after a descriptor walk
1358 // confirmed no `.then` accessor in the chain. The
1359 // external review demonstrated a counter: a getter
1360 // that counts reads, returns non-function on each
1361 // pre-read, then self-replaces with a data property
1362 // holding a malicious function before V8's `[[Get]]` in
1363 // PromiseResolveThenableJob. By the time the descriptor
1364 // walk runs, the getter has already mutated to a data
1365 // property, so the walk reports "no accessor" and the
1366 // code passes `value` to `resolve()`. V8 then reads the
1367 // malicious function via [[Get]] and schedules another
1368 // PromiseResolveThenableJob that calls it OUTSIDE our
1369 // wrapper — proven by the V8-supplied resolver having
1370 // `.name === ""` instead of `safeResolveCallback`.
1371 //
1372 // Doubling, tripling, or N-reading does not help: the
1373 // getter (or a Proxy) can count to any N before
1374 // switching, and Proxies can lie about descriptors.
1375 // Detection-based heuristics on an attacker-controlled
1376 // `.then` slot are fundamentally bypassable.
1377 //
1378 // Structural fix: when `userThen` is non-function on
1379 // our read, ALWAYS resolve with a sandbox-realm shadow
1380 // that has no `.then` anywhere in its chain. V8 cannot
1381 // re-read the user's `value`; it only sees the shadow,
1382 // which we fully control. Trade-off: identity is not
1383 // preserved for non-thenable values passed to
1384 // `i.return(x)` (the resolved iterator value will not
1385 // be `===` to the input). Identity preservation in this
1386 // codepath is incompatible with safety against TOCTOU
1387 // attacks on `.then`; the shadow option is the only
1388 // invariant we can hold against an adversarial input.

Callers 1

wrapAsyncGenMethodFunction · 0.85

Calls 4

safeSanitizeFunction · 0.85
makeNonThenableShadowFunction · 0.85
resolveFunction · 0.85
applyFunction · 0.85

Tested by

no test coverage detected

Used in the wild real call sites across dependent graphs

searching dependent graphs…