Skip to content

Commit 29f34a7

Browse files
dario-piotrowicztargos
authored andcommitted
lib: disable REPL completion on proxies and getters
the REPL completion logic evaluates code, this is generally ok but it can be problematic when there are objects with nested properties since the code evaluation would trigger their potential getters (e.g. the `obj.foo.b` line would trigger the getter of `foo`), the changes here disable the completion logic when proxies and getters are involved thus making sure that code evaluation does not take place when it can potentially (and surprisingly to the user) trigger side effectful behaviors PR-URL: #57909 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
1 parent 1337751 commit 29f34a7

File tree

4 files changed

+859
-657
lines changed

4 files changed

+859
-657
lines changed

lib/repl.js

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ const {
9696
globalThis,
9797
} = primordials;
9898

99+
const {
100+
isProxy,
101+
} = require('internal/util/types');
102+
99103
const { BuiltinModule } = require('internal/bootstrap/realm');
100104
const {
101105
makeRequireFunction,
@@ -1328,8 +1332,10 @@ function completeFSFunctions(match) {
13281332
// -> [['util.print', 'util.debug', 'util.log', 'util.inspect'],
13291333
// 'util.' ]
13301334
//
1331-
// Warning: This eval's code like "foo.bar.baz", so it will run property
1332-
// getter code.
1335+
// Warning: This evals code like "foo.bar.baz", so it could run property
1336+
// getter code. To avoid potential triggering side-effects with getters the completion
1337+
// logic is skipped when getters or proxies are involved in the expression.
1338+
// (see: https://github.com/nodejs/node/issues/57829).
13331339
function complete(line, callback) {
13341340
// List of completion lists, one for each inheritance "level"
13351341
let completionGroups = [];
@@ -1525,50 +1531,61 @@ function complete(line, callback) {
15251531
return;
15261532
}
15271533

1528-
let chaining = '.';
1529-
if (StringPrototypeEndsWith(expr, '?')) {
1530-
expr = StringPrototypeSlice(expr, 0, -1);
1531-
chaining = '?.';
1532-
}
1533-
1534-
const memberGroups = [];
1535-
const evalExpr = `try { ${expr} } catch {}`;
1536-
this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
1537-
try {
1538-
let p;
1539-
if ((typeof obj === 'object' && obj !== null) ||
1540-
typeof obj === 'function') {
1541-
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
1542-
p = ObjectGetPrototypeOf(obj);
1543-
} else {
1544-
p = obj.constructor ? obj.constructor.prototype : null;
1534+
return includesProxiesOrGetters(
1535+
StringPrototypeSplit(match, '.'),
1536+
this.eval,
1537+
this.context,
1538+
(includes) => {
1539+
if (includes) {
1540+
// The expression involves proxies or getters, meaning that it
1541+
// can trigger side-effectful behaviors, so bail out
1542+
return completionGroupsLoaded();
15451543
}
1546-
// Circular refs possible? Let's guard against that.
1547-
let sentinel = 5;
1548-
while (p !== null && sentinel-- !== 0) {
1549-
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p));
1550-
p = ObjectGetPrototypeOf(p);
1544+
1545+
let chaining = '.';
1546+
if (StringPrototypeEndsWith(expr, '?')) {
1547+
expr = StringPrototypeSlice(expr, 0, -1);
1548+
chaining = '?.';
15511549
}
1552-
} catch {
1553-
// Maybe a Proxy object without `getOwnPropertyNames` trap.
1554-
// We simply ignore it here, as we don't want to break the
1555-
// autocompletion. Fixes the bug
1556-
// https://github.com/nodejs/node/issues/2119
1557-
}
15581550

1559-
if (memberGroups.length) {
1560-
expr += chaining;
1561-
ArrayPrototypeForEach(memberGroups, (group) => {
1562-
ArrayPrototypePush(completionGroups,
1563-
ArrayPrototypeMap(group,
1564-
(member) => `${expr}${member}`));
1565-
});
1566-
filter &&= `${expr}${filter}`;
1567-
}
1551+
const memberGroups = [];
1552+
const evalExpr = `try { ${expr} } catch {}`;
1553+
this.eval(evalExpr, this.context, getREPLResourceName(), (e, obj) => {
1554+
try {
1555+
let p;
1556+
if ((typeof obj === 'object' && obj !== null) ||
1557+
typeof obj === 'function') {
1558+
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(obj));
1559+
p = ObjectGetPrototypeOf(obj);
1560+
} else {
1561+
p = obj.constructor ? obj.constructor.prototype : null;
1562+
}
1563+
// Circular refs possible? Let's guard against that.
1564+
let sentinel = 5;
1565+
while (p !== null && sentinel-- !== 0) {
1566+
ArrayPrototypePush(memberGroups, filteredOwnPropertyNames(p));
1567+
p = ObjectGetPrototypeOf(p);
1568+
}
1569+
} catch {
1570+
// Maybe a Proxy object without `getOwnPropertyNames` trap.
1571+
// We simply ignore it here, as we don't want to break the
1572+
// autocompletion. Fixes the bug
1573+
// https://github.com/nodejs/node/issues/2119
1574+
}
15681575

1569-
completionGroupsLoaded();
1570-
});
1571-
return;
1576+
if (memberGroups.length) {
1577+
expr += chaining;
1578+
ArrayPrototypeForEach(memberGroups, (group) => {
1579+
ArrayPrototypePush(completionGroups,
1580+
ArrayPrototypeMap(group,
1581+
(member) => `${expr}${member}`));
1582+
});
1583+
filter &&= `${expr}${filter}`;
1584+
}
1585+
1586+
completionGroupsLoaded();
1587+
});
1588+
});
15721589
}
15731590

15741591
return completionGroupsLoaded();
@@ -1626,6 +1643,34 @@ function complete(line, callback) {
16261643
}
16271644
}
16281645

1646+
function includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr = '', idx = 0) {
1647+
const currentSegment = exprSegments[idx];
1648+
currentExpr += `${currentExpr.length === 0 ? '' : '.'}${currentSegment}`;
1649+
evalFn(`try { ${currentExpr} } catch { }`, context, getREPLResourceName(), (_, currentObj) => {
1650+
if (typeof currentObj !== 'object' || currentObj === null) {
1651+
return callback(false);
1652+
}
1653+
1654+
if (isProxy(currentObj)) {
1655+
return callback(true);
1656+
}
1657+
1658+
const nextIdx = idx + 1;
1659+
1660+
if (nextIdx >= exprSegments.length) {
1661+
return callback(false);
1662+
}
1663+
1664+
const nextSegmentProp = ObjectGetOwnPropertyDescriptor(currentObj, exprSegments[nextIdx]);
1665+
const nextSegmentPropHasGetter = typeof nextSegmentProp?.get === 'function';
1666+
if (nextSegmentPropHasGetter) {
1667+
return callback(true);
1668+
}
1669+
1670+
return includesProxiesOrGetters(exprSegments, evalFn, context, callback, currentExpr, nextIdx);
1671+
});
1672+
}
1673+
16291674
REPLServer.prototype.completeOnEditorMode = (callback) => (err, results) => {
16301675
if (err) return callback(err);
16311676

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('node:assert');
5+
const { describe, test } = require('node:test');
6+
7+
const ArrayStream = require('../common/arraystream');
8+
9+
const repl = require('node:repl');
10+
11+
function runCompletionTests(replInit, tests) {
12+
const stream = new ArrayStream();
13+
const testRepl = repl.start({ stream });
14+
15+
// Some errors are passed to the domain
16+
testRepl._domain.on('error', assert.ifError);
17+
18+
testRepl.write(replInit);
19+
testRepl.write('\n');
20+
21+
tests.forEach(([query, expectedCompletions]) => {
22+
testRepl.complete(query, common.mustCall((error, data) => {
23+
const actualCompletions = data[0];
24+
if (expectedCompletions.length === 0) {
25+
assert.deepStrictEqual(actualCompletions, []);
26+
} else {
27+
expectedCompletions.forEach((expectedCompletion) =>
28+
assert(actualCompletions.includes(expectedCompletion), `completion '${expectedCompletion}' not found`)
29+
);
30+
}
31+
}));
32+
});
33+
}
34+
35+
describe('REPL completion in relation of getters', () => {
36+
describe('standard behavior without proxies/getters', () => {
37+
test('completion of nested properties of an undeclared objects', () => {
38+
runCompletionTests('', [
39+
['nonExisting.', []],
40+
['nonExisting.f', []],
41+
['nonExisting.foo', []],
42+
['nonExisting.foo.', []],
43+
['nonExisting.foo.bar.b', []],
44+
]);
45+
});
46+
47+
test('completion of nested properties on plain objects', () => {
48+
runCompletionTests('const plainObj = { foo: { bar: { baz: {} } } };', [
49+
['plainObj.', ['plainObj.foo']],
50+
['plainObj.f', ['plainObj.foo']],
51+
['plainObj.foo', ['plainObj.foo']],
52+
['plainObj.foo.', ['plainObj.foo.bar']],
53+
['plainObj.foo.bar.b', ['plainObj.foo.bar.baz']],
54+
['plainObj.fooBar.', []],
55+
['plainObj.fooBar.baz', []],
56+
]);
57+
});
58+
});
59+
60+
describe('completions on an object with getters', () => {
61+
test(`completions are generated for properties that don't trigger getters`, () => {
62+
runCompletionTests(
63+
`
64+
const objWithGetters = {
65+
foo: { bar: { baz: {} }, get gBar() { return { baz: {} } } },
66+
get gFoo() { return { bar: { baz: {} } }; }
67+
};
68+
`, [
69+
['objWithGetters.', ['objWithGetters.foo']],
70+
['objWithGetters.f', ['objWithGetters.foo']],
71+
['objWithGetters.foo', ['objWithGetters.foo']],
72+
['objWithGetters.foo.', ['objWithGetters.foo.bar']],
73+
['objWithGetters.foo.bar.b', ['objWithGetters.foo.bar.baz']],
74+
['objWithGetters.gFo', ['objWithGetters.gFoo']],
75+
['objWithGetters.foo.gB', ['objWithGetters.foo.gBar']],
76+
]);
77+
});
78+
79+
test('no completions are generated for properties that trigger getters', () => {
80+
runCompletionTests(
81+
`
82+
const objWithGetters = {
83+
foo: { bar: { baz: {} }, get gBar() { return { baz: {} } } },
84+
get gFoo() { return { bar: { baz: {} } }; }
85+
};
86+
`,
87+
[
88+
['objWithGetters.gFoo.', []],
89+
['objWithGetters.gFoo.b', []],
90+
['objWithGetters.gFoo.bar.b', []],
91+
['objWithGetters.foo.gBar.', []],
92+
['objWithGetters.foo.gBar.b', []],
93+
]);
94+
});
95+
});
96+
97+
describe('completions on proxies', () => {
98+
test('no completions are generated for a proxy object', () => {
99+
runCompletionTests('const proxyObj = new Proxy({ foo: { bar: { baz: {} } } }, {});', [
100+
['proxyObj.', []],
101+
['proxyObj.f', []],
102+
['proxyObj.foo', []],
103+
['proxyObj.foo.', []],
104+
['proxyObj.foo.bar.b', []],
105+
]);
106+
});
107+
108+
test('no completions are generated for a proxy present in a standard object', () => {
109+
runCompletionTests(
110+
'const objWithProxy = { foo: { bar: new Proxy({ baz: {} }, {}) } };', [
111+
['objWithProxy.', ['objWithProxy.foo']],
112+
['objWithProxy.foo', ['objWithProxy.foo']],
113+
['objWithProxy.foo.', ['objWithProxy.foo.bar']],
114+
['objWithProxy.foo.b', ['objWithProxy.foo.bar']],
115+
['objWithProxy.foo.bar.', []],
116+
]);
117+
});
118+
});
119+
});

0 commit comments

Comments
 (0)