Skip to content

Commit 5cbe7c2

Browse files
addaleaxtargos
authored andcommitted
process: make source map getter resistant against prototype tampering
Since this code runs during process and Worker shutdown, it should not call user-provided code and thereby e.g. provide a way to break out of `worker.terminate()`. PR-URL: #30228 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Ben Coe <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 338d216 commit 5cbe7c2

File tree

2 files changed

+91
-12
lines changed

2 files changed

+91
-12
lines changed

lib/internal/source_map/source_map_cache.js

+51-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
'use strict';
22

3+
const {
4+
JSON,
5+
Object: {
6+
create: ObjectCreate,
7+
keys: ObjectKeys,
8+
getOwnPropertyDescriptor: ObjectGetOwnPropertyDescriptor,
9+
},
10+
ObjectPrototype: {
11+
hasOwnProperty: ObjectHasOwnProperty
12+
},
13+
MapPrototype: {
14+
entries: MapEntries
15+
}, uncurryThis
16+
} = primordials;
17+
18+
const MapIteratorNext = uncurryThis(MapEntries(new Map()).next);
19+
const WeakMapGet = uncurryThis(WeakMap.prototype.get);
20+
21+
function ObjectGetValueSafe(obj, key) {
22+
const desc = ObjectGetOwnPropertyDescriptor(obj, key);
23+
return ObjectHasOwnProperty(desc, 'value') ? desc.value : undefined;
24+
}
25+
326
// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
427
const { Buffer } = require('buffer');
528
const debug = require('internal/util/debuglog').debuglog('source_map');
@@ -9,14 +32,14 @@ const { getOptionValue } = require('internal/options');
932
const {
1033
normalizeReferrerURL,
1134
} = require('internal/modules/cjs/helpers');
12-
const { JSON, Object } = primordials;
1335
// For cjs, since Module._cache is exposed to users, we use a WeakMap
1436
// keyed on module, facilitating garbage collection.
1537
const cjsSourceMapCache = new WeakMap();
1638
// The esm cache is not exposed to users, so we can use a Map keyed
1739
// on filenames.
1840
const esmSourceMapCache = new Map();
1941
const { fileURLToPath, URL } = require('url');
42+
let Module;
2043

2144
let experimentalSourceMaps;
2245
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
@@ -40,6 +63,7 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
4063
const data = dataFromUrl(basePath, match.groups.sourceMappingURL);
4164
const url = data ? null : match.groups.sourceMappingURL;
4265
if (cjsModuleInstance) {
66+
if (!Module) Module = require('internal/modules/cjs/loader').Module;
4367
cjsSourceMapCache.set(cjsModuleInstance, {
4468
filename,
4569
lineLengths: lineLengths(content),
@@ -148,17 +172,27 @@ function rekeySourceMap(cjsModuleInstance, newInstance) {
148172
}
149173
}
150174

175+
// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
176+
// shutdown. In particular, they also run when Workers are terminated, making
177+
// it important that they do not call out to any user-provided code, including
178+
// built-in prototypes that might have been tampered with.
179+
151180
// Get serialized representation of source-map cache, this is used
152181
// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
153182
function sourceMapCacheToObject() {
154-
const obj = Object.create(null);
183+
const obj = ObjectCreate(null);
155184

156-
for (const [k, v] of esmSourceMapCache) {
185+
const it = MapEntries(esmSourceMapCache);
186+
let entry;
187+
while (!(entry = MapIteratorNext(it)).done) {
188+
const k = entry.value[0];
189+
const v = entry.value[1];
157190
obj[k] = v;
158191
}
192+
159193
appendCJSCache(obj);
160194

161-
if (Object.keys(obj).length === 0) {
195+
if (ObjectKeys(obj).length === 0) {
162196
return undefined;
163197
} else {
164198
return obj;
@@ -171,23 +205,28 @@ function sourceMapCacheToObject() {
171205
// TODO(bcoe): this means we don't currently serialize source-maps attached
172206
// to error instances, only module instances.
173207
function appendCJSCache(obj) {
174-
const { Module } = require('internal/modules/cjs/loader');
175-
Object.keys(Module._cache).forEach((key) => {
176-
const value = cjsSourceMapCache.get(Module._cache[key]);
208+
if (!Module) return;
209+
const cjsModuleCache = ObjectGetValueSafe(Module, '_cache');
210+
const cjsModules = ObjectKeys(cjsModuleCache);
211+
for (let i = 0; i < cjsModules.length; i++) {
212+
const key = cjsModules[i];
213+
const module = ObjectGetValueSafe(cjsModuleCache, key);
214+
const value = WeakMapGet(cjsSourceMapCache, module);
177215
if (value) {
216+
// This is okay because `obj` has a null prototype.
178217
obj[`file://${key}`] = {
179-
lineLengths: value.lineLengths,
180-
data: value.data,
181-
url: value.url
218+
lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
219+
data: ObjectGetValueSafe(value, 'data'),
220+
url: ObjectGetValueSafe(value, 'url')
182221
};
183222
}
184-
});
223+
}
185224
}
186225

187226
// Attempt to lookup a source map, which is either attached to a file URI, or
188227
// keyed on an error instance.
189228
function findSourceMap(uri, error) {
190-
const { Module } = require('internal/modules/cjs/loader');
229+
if (!Module) Module = require('internal/modules/cjs/loader').Module;
191230
let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
192231
if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
193232
if (sourceMap === undefined) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
const common = require('../common');
3+
const tmpdir = require('../common/tmpdir');
4+
const assert = require('assert');
5+
6+
// Attempts to test that the source map JS code run on process shutdown
7+
// does not call any user-defined JS code.
8+
9+
const { Worker, workerData, parentPort } = require('worker_threads');
10+
11+
if (!workerData) {
12+
tmpdir.refresh();
13+
process.env.NODE_V8_COVERAGE = tmpdir.path;
14+
15+
// Count the number of some calls that should not be made.
16+
const callCount = new Int32Array(new SharedArrayBuffer(4));
17+
const w = new Worker(__filename, { workerData: { callCount } });
18+
w.on('message', common.mustCall(() => w.terminate()));
19+
w.on('exit', common.mustCall(() => {
20+
assert.strictEqual(callCount[0], 0);
21+
}));
22+
return;
23+
}
24+
25+
const { callCount } = workerData;
26+
27+
function increaseCallCount() { callCount[0]++; }
28+
29+
// Increase the call count when a forbidden method is called.
30+
Object.getPrototypeOf((new Map()).entries()).next = increaseCallCount;
31+
Map.prototype.entries = increaseCallCount;
32+
Object.keys = increaseCallCount;
33+
Object.create = increaseCallCount;
34+
Object.hasOwnProperty = increaseCallCount;
35+
Object.defineProperty(Object.prototype, 'value', {
36+
get: increaseCallCount,
37+
set: increaseCallCount
38+
});
39+
40+
parentPort.postMessage('done');

0 commit comments

Comments
 (0)