Skip to content

Commit d7bb76f

Browse files
committed
feat: US-119-B - Block module cache poisoning within single execution
1 parent a74c02d commit d7bb76f

4 files changed

Lines changed: 203 additions & 39 deletions

File tree

packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@
344344
return stub;
345345
}
346346

347+
// Capture the real module cache for internal use before exposing a read-only view
348+
const __internalModuleCache = _moduleCache;
349+
347350
const __require = function require(moduleName) {
348351
return _requireFrom(moduleName, _currentModule.dirname);
349352
};
@@ -362,7 +365,6 @@
362365
globalThis.require.resolve = function resolve(moduleName) {
363366
return _resolveFrom(moduleName, _currentModule.dirname);
364367
};
365-
globalThis.require.cache = _moduleCache;
366368

367369
function _debugRequire(phase, moduleName, extra) {
368370
if (globalThis.__sandboxRequireDebug !== true) {
@@ -403,34 +405,34 @@
403405
const isRelative = name.startsWith('./') || name.startsWith('../');
404406

405407
// Get cached modules for bare/absolute specifiers up front.
406-
if (!isRelative && _moduleCache[name]) {
408+
if (!isRelative && __internalModuleCache[name]) {
407409
_debugRequire('cache-hit', name, name);
408-
return _moduleCache[name];
410+
return __internalModuleCache[name];
409411
}
410412

411413
// Special handling for fs module
412414
if (name === 'fs') {
413-
if (_moduleCache['fs']) return _moduleCache['fs'];
415+
if (__internalModuleCache['fs']) return __internalModuleCache['fs'];
414416
const fsModule = globalThis.bridge?.fs || globalThis.bridge?.default || globalThis._fsModule || {};
415-
_moduleCache['fs'] = fsModule;
417+
__internalModuleCache['fs'] = fsModule;
416418
_debugRequire('loaded', name, 'fs-special');
417419
return fsModule;
418420
}
419421

420422
// Special handling for fs/promises module
421423
if (name === 'fs/promises') {
422-
if (_moduleCache['fs/promises']) return _moduleCache['fs/promises'];
424+
if (__internalModuleCache['fs/promises']) return __internalModuleCache['fs/promises'];
423425
// Get fs module first, then extract promises
424426
const fsModule = _requireFrom('fs', fromDir);
425-
_moduleCache['fs/promises'] = fsModule.promises;
427+
__internalModuleCache['fs/promises'] = fsModule.promises;
426428
_debugRequire('loaded', name, 'fs-promises-special');
427429
return fsModule.promises;
428430
}
429431

430432
// Special handling for stream/promises module.
431433
// Expose promise-based wrappers backed by stream callback APIs.
432434
if (name === 'stream/promises') {
433-
if (_moduleCache['stream/promises']) return _moduleCache['stream/promises'];
435+
if (__internalModuleCache['stream/promises']) return __internalModuleCache['stream/promises'];
434436
const streamModule = _requireFrom('stream', fromDir);
435437
const promisesModule = {
436438
finished(stream, options) {
@@ -482,63 +484,63 @@
482484
});
483485
},
484486
};
485-
_moduleCache['stream/promises'] = promisesModule;
487+
__internalModuleCache['stream/promises'] = promisesModule;
486488
_debugRequire('loaded', name, 'stream-promises-special');
487489
return promisesModule;
488490
}
489491

490492
// Special handling for child_process module
491493
if (name === 'child_process') {
492-
if (_moduleCache['child_process']) return _moduleCache['child_process'];
493-
_moduleCache['child_process'] = _childProcessModule;
494+
if (__internalModuleCache['child_process']) return __internalModuleCache['child_process'];
495+
__internalModuleCache['child_process'] = _childProcessModule;
494496
_debugRequire('loaded', name, 'child-process-special');
495497
return _childProcessModule;
496498
}
497499

498500
// Special handling for http module
499501
if (name === 'http') {
500-
if (_moduleCache['http']) return _moduleCache['http'];
501-
_moduleCache['http'] = _httpModule;
502+
if (__internalModuleCache['http']) return __internalModuleCache['http'];
503+
__internalModuleCache['http'] = _httpModule;
502504
_debugRequire('loaded', name, 'http-special');
503505
return _httpModule;
504506
}
505507

506508
// Special handling for https module
507509
if (name === 'https') {
508-
if (_moduleCache['https']) return _moduleCache['https'];
509-
_moduleCache['https'] = _httpsModule;
510+
if (__internalModuleCache['https']) return __internalModuleCache['https'];
511+
__internalModuleCache['https'] = _httpsModule;
510512
_debugRequire('loaded', name, 'https-special');
511513
return _httpsModule;
512514
}
513515

514516
// Special handling for http2 module
515517
if (name === 'http2') {
516-
if (_moduleCache['http2']) return _moduleCache['http2'];
517-
_moduleCache['http2'] = _http2Module;
518+
if (__internalModuleCache['http2']) return __internalModuleCache['http2'];
519+
__internalModuleCache['http2'] = _http2Module;
518520
_debugRequire('loaded', name, 'http2-special');
519521
return _http2Module;
520522
}
521523

522524
// Special handling for dns module
523525
if (name === 'dns') {
524-
if (_moduleCache['dns']) return _moduleCache['dns'];
525-
_moduleCache['dns'] = _dnsModule;
526+
if (__internalModuleCache['dns']) return __internalModuleCache['dns'];
527+
__internalModuleCache['dns'] = _dnsModule;
526528
_debugRequire('loaded', name, 'dns-special');
527529
return _dnsModule;
528530
}
529531

530532
// Special handling for os module
531533
if (name === 'os') {
532-
if (_moduleCache['os']) return _moduleCache['os'];
533-
_moduleCache['os'] = _osModule;
534+
if (__internalModuleCache['os']) return __internalModuleCache['os'];
535+
__internalModuleCache['os'] = _osModule;
534536
_debugRequire('loaded', name, 'os-special');
535537
return _osModule;
536538
}
537539

538540
// Special handling for module module
539541
if (name === 'module') {
540-
if (_moduleCache['module']) return _moduleCache['module'];
541-
_moduleCache['module'] = _moduleModule;
542+
if (__internalModuleCache['module']) return __internalModuleCache['module'];
543+
__internalModuleCache['module'] = _moduleModule;
542544
_debugRequire('loaded', name, 'module-special');
543545
return _moduleModule;
544546
}
@@ -553,7 +555,7 @@
553555
// Special handling for async_hooks.
554556
// This provides the minimum API surface needed by tracing libraries.
555557
if (name === 'async_hooks') {
556-
if (_moduleCache['async_hooks']) return _moduleCache['async_hooks'];
558+
if (__internalModuleCache['async_hooks']) return __internalModuleCache['async_hooks'];
557559

558560
class AsyncLocalStorage {
559561
constructor() {
@@ -622,14 +624,14 @@
622624
executionAsyncResource() { return null; },
623625
};
624626

625-
_moduleCache['async_hooks'] = asyncHooksModule;
627+
__internalModuleCache['async_hooks'] = asyncHooksModule;
626628
_debugRequire('loaded', name, 'async-hooks-special');
627629
return asyncHooksModule;
628630
}
629631

630632
// No-op diagnostics_channel stub — channels report no subscribers
631633
if (name === 'diagnostics_channel') {
632-
if (_moduleCache[name]) return _moduleCache[name];
634+
if (__internalModuleCache[name]) return __internalModuleCache[name];
633635

634636
function _createChannel() {
635637
return {
@@ -660,16 +662,16 @@
660662
},
661663
};
662664

663-
_moduleCache[name] = dcModule;
665+
__internalModuleCache[name] = dcModule;
664666
_debugRequire('loaded', name, 'diagnostics-channel-special');
665667
return dcModule;
666668
}
667669

668670
// Get deferred module stubs
669671
if (_deferredCoreModules.has(name)) {
670-
if (_moduleCache[name]) return _moduleCache[name];
672+
if (__internalModuleCache[name]) return __internalModuleCache[name];
671673
const deferredStub = _createDeferredModuleStub(name);
672-
_moduleCache[name] = deferredStub;
674+
__internalModuleCache[name] = deferredStub;
673675
_debugRequire('loaded', name, 'deferred-stub');
674676
return deferredStub;
675677
}
@@ -682,7 +684,7 @@
682684
// Try to load polyfill first (for built-in modules like path, events, etc.)
683685
const polyfillCode = _loadPolyfill.applySyncPromise(undefined, [name]);
684686
if (polyfillCode !== null) {
685-
if (_moduleCache[name]) return _moduleCache[name];
687+
if (__internalModuleCache[name]) return __internalModuleCache[name];
686688

687689
const moduleObj = { exports: {} };
688690
_pendingModules[name] = moduleObj;
@@ -695,10 +697,10 @@
695697
moduleObj.exports = result;
696698
}
697699

698-
_moduleCache[name] = moduleObj.exports;
700+
__internalModuleCache[name] = moduleObj.exports;
699701
delete _pendingModules[name];
700702
_debugRequire('loaded', name, 'polyfill');
701-
return _moduleCache[name];
703+
return __internalModuleCache[name];
702704
}
703705

704706
// Resolve module path using host-side resolution
@@ -708,9 +710,9 @@
708710
cacheKey = resolved;
709711

710712
// Check cache with resolved path
711-
if (_moduleCache[cacheKey]) {
713+
if (__internalModuleCache[cacheKey]) {
712714
_debugRequire('cache-hit', name, cacheKey);
713-
return _moduleCache[cacheKey];
715+
return __internalModuleCache[cacheKey];
714716
}
715717

716718
// Check if we're currently loading this module (circular dep)
@@ -730,7 +732,7 @@
730732
// Handle JSON files
731733
if (resolved.endsWith('.json')) {
732734
const parsed = JSON.parse(source);
733-
_moduleCache[cacheKey] = parsed;
735+
__internalModuleCache[cacheKey] = parsed;
734736
return parsed;
735737
}
736738

@@ -813,7 +815,7 @@
813815
}
814816

815817
// Cache with resolved path
816-
_moduleCache[cacheKey] = module.exports;
818+
__internalModuleCache[cacheKey] = module.exports;
817819
delete _pendingModules[cacheKey];
818820
_debugRequire('loaded', name, cacheKey);
819821

@@ -822,3 +824,52 @@
822824

823825
// Expose _requireFrom globally so module polyfill can access it
824826
__requireExposeCustomGlobal("_requireFrom", _requireFrom);
827+
828+
// Block module cache poisoning: create a read-only Proxy over the real cache.
829+
// Internal require writes go through __internalModuleCache (captured above);
830+
// sandbox code sees only this Proxy which rejects set/delete/defineProperty.
831+
const __moduleCacheProxy = new Proxy(__internalModuleCache, {
832+
get(target, prop, receiver) {
833+
return Reflect.get(target, prop, receiver);
834+
},
835+
set(_target, prop) {
836+
throw new TypeError("Cannot set require.cache['" + String(prop) + "']");
837+
},
838+
deleteProperty(_target, prop) {
839+
throw new TypeError("Cannot delete require.cache['" + String(prop) + "']");
840+
},
841+
defineProperty(_target, prop) {
842+
throw new TypeError("Cannot define property '" + String(prop) + "' on require.cache");
843+
},
844+
has(target, prop) {
845+
return Reflect.has(target, prop);
846+
},
847+
ownKeys(target) {
848+
return Reflect.ownKeys(target);
849+
},
850+
getOwnPropertyDescriptor(target, prop) {
851+
return Reflect.getOwnPropertyDescriptor(target, prop);
852+
},
853+
});
854+
855+
// Expose read-only proxy as require.cache
856+
globalThis.require.cache = __moduleCacheProxy;
857+
858+
// Replace _moduleCache global with read-only proxy so sandbox code
859+
// cannot bypass require.cache protection via the raw global.
860+
// Keep configurable:true — applyCustomGlobalExposurePolicy will lock it
861+
// down to non-configurable after all bridge setup completes.
862+
Object.defineProperty(globalThis, '_moduleCache', {
863+
value: __moduleCacheProxy,
864+
writable: false,
865+
configurable: true,
866+
enumerable: false,
867+
});
868+
869+
// Update Module._cache references to use the read-only proxy
870+
if (typeof _moduleModule !== 'undefined') {
871+
if (_moduleModule.Module) {
872+
_moduleModule.Module._cache = __moduleCacheProxy;
873+
}
874+
_moduleModule._cache = __moduleCacheProxy;
875+
}

0 commit comments

Comments
 (0)