(value)
| 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. |
no test coverage detected
searching dependent graphs…