Skip to content

Commit 6c013d4

Browse files
committed
fix: Arbitrary code execution at prettier config load
1 parent ebaf83f commit 6c013d4

File tree

1 file changed

+170
-15
lines changed

1 file changed

+170
-15
lines changed

src/lib/prettierFormatter.js

Lines changed: 170 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fsOperation from "fileSystem";
2+
import { parse } from "acorn";
23
import toast from "components/toast";
34
import appSettings from "lib/settings";
45
import prettierPluginBabel from "prettier/plugins/babel";
@@ -319,7 +320,7 @@ function parseJsonLike(text) {
319320
const parsed = helpers.parseJSON(trimmed);
320321
if (parsed) return parsed;
321322
try {
322-
return new Function(`return (${trimmed});`)();
323+
return parseSafeExpression(trimmed);
323324
} catch (_) {
324325
return null;
325326
}
@@ -329,26 +330,180 @@ function parseJsConfig(directory, source, absolutePath) {
329330
if (!source) return null;
330331
void directory;
331332
void absolutePath;
332-
let transformed = source;
333-
if (/export\s+default/.test(transformed)) {
334-
transformed = transformed.replace(/export\s+default/, "module.exports =");
335-
}
336-
const module = { exports: {} };
337-
const exports = module.exports;
338-
function requireStub(request) {
339-
throw new Error(
340-
`require(\"${request}\") is not supported in Prettier configs inside Acode`,
341-
);
342-
}
343333
try {
344-
const fn = new Function("module", "exports", "require", transformed);
345-
fn(module, exports, requireStub);
346-
return module.exports ?? exports;
334+
return extractConfigFromProgram(source);
347335
} catch (_) {
348336
return null;
349337
}
350338
}
351339

340+
function parseProgram(source) {
341+
try {
342+
return parse(source, {
343+
ecmaVersion: "latest",
344+
sourceType: "module",
345+
allowHashBang: true,
346+
});
347+
} catch (_) {
348+
return parse(source, {
349+
ecmaVersion: "latest",
350+
sourceType: "script",
351+
allowHashBang: true,
352+
});
353+
}
354+
}
355+
356+
function extractConfigFromProgram(source) {
357+
const ast = parseProgram(source);
358+
const scope = new Map();
359+
360+
for (const statement of ast.body) {
361+
const declared = readVariableDeclaration(statement, scope);
362+
if (declared) {
363+
for (const [name, value] of declared) {
364+
scope.set(name, value);
365+
}
366+
continue;
367+
}
368+
369+
const exported = readCommonJsExport(statement, scope);
370+
if (exported !== undefined) return exported;
371+
372+
const esmExported = readEsmExport(statement, scope);
373+
if (esmExported !== undefined) return esmExported;
374+
}
375+
376+
return null;
377+
}
378+
379+
function parseSafeExpression(text) {
380+
const wrapped = `(${text})`;
381+
const ast = parse(wrapped, {
382+
ecmaVersion: "latest",
383+
sourceType: "module",
384+
allowHashBang: true,
385+
});
386+
const statement = ast.body[0];
387+
if (statement?.type !== "ExpressionStatement") return null;
388+
return evaluateNode(statement.expression, new Map());
389+
}
390+
391+
function readVariableDeclaration(statement, scope) {
392+
if (statement?.type !== "VariableDeclaration") return null;
393+
const values = new Map();
394+
const lookupScope = new Map(scope);
395+
396+
for (const decl of statement.declarations || []) {
397+
if (!decl || decl.type !== "VariableDeclarator") continue;
398+
if (decl.id?.type !== "Identifier") continue;
399+
if (!decl.init) continue;
400+
try {
401+
const value = evaluateNode(decl.init, lookupScope);
402+
values.set(decl.id.name, value);
403+
lookupScope.set(decl.id.name, value);
404+
} catch (_) {
405+
// Ignore unsupported declarations
406+
}
407+
}
408+
409+
return values.size ? values : null;
410+
}
411+
412+
function readCommonJsExport(statement, scope) {
413+
if (statement?.type !== "ExpressionStatement") return undefined;
414+
const expr = statement.expression;
415+
if (expr?.type !== "AssignmentExpression" || expr.operator !== "=") {
416+
return undefined;
417+
}
418+
419+
if (!isModuleExports(expr.left)) return undefined;
420+
return evaluateNode(expr.right, scope);
421+
}
422+
423+
function readEsmExport(statement, scope) {
424+
if (statement?.type !== "ExportDefaultDeclaration") return undefined;
425+
return evaluateNode(statement.declaration, scope);
426+
}
427+
428+
function isModuleExports(node) {
429+
return (
430+
node?.type === "MemberExpression" &&
431+
!node.computed &&
432+
node.object?.type === "Identifier" &&
433+
node.object.name === "module" &&
434+
node.property?.type === "Identifier" &&
435+
node.property.name === "exports"
436+
);
437+
}
438+
439+
function evaluateNode(node, scope) {
440+
if (!node) return null;
441+
442+
switch (node.type) {
443+
case "ObjectExpression":
444+
return evaluateObjectExpression(node, scope);
445+
case "ArrayExpression":
446+
return node.elements.map((entry) => evaluateNode(entry, scope));
447+
case "Literal":
448+
return node.value;
449+
case "TemplateLiteral":
450+
if (node.expressions.length) {
451+
throw new Error("Template expressions are not supported");
452+
}
453+
return node.quasis.map((part) => part.value.cooked ?? "").join("");
454+
case "Identifier":
455+
if (scope.has(node.name)) return scope.get(node.name);
456+
if (node.name === "undefined") return undefined;
457+
throw new Error(`Unsupported identifier: ${node.name}`);
458+
case "UnaryExpression":
459+
return evaluateUnaryExpression(node, scope);
460+
default:
461+
throw new Error(`Unsupported node type: ${node.type}`);
462+
}
463+
}
464+
465+
function evaluateObjectExpression(node, scope) {
466+
const output = {};
467+
for (const property of node.properties || []) {
468+
if (!property || property.type !== "Property") {
469+
throw new Error("Unsupported object property");
470+
}
471+
if (property.kind !== "init" || property.method || property.shorthand) {
472+
throw new Error("Unsupported object property kind");
473+
}
474+
const key = property.computed
475+
? evaluateNode(property.key, scope)
476+
: getPropertyKey(property.key);
477+
const normalizedKey =
478+
typeof key === "string" || typeof key === "number" ? String(key) : null;
479+
if (!normalizedKey) {
480+
throw new Error("Unsupported object key");
481+
}
482+
output[normalizedKey] = evaluateNode(property.value, scope);
483+
}
484+
return output;
485+
}
486+
487+
function getPropertyKey(node) {
488+
if (node?.type === "Identifier") return node.name;
489+
if (node?.type === "Literal") return node.value;
490+
throw new Error("Unsupported property key");
491+
}
492+
493+
function evaluateUnaryExpression(node, scope) {
494+
const value = evaluateNode(node.argument, scope);
495+
switch (node.operator) {
496+
case "+":
497+
return +value;
498+
case "-":
499+
return -value;
500+
case "!":
501+
return !value;
502+
default:
503+
throw new Error(`Unsupported unary operator: ${node.operator}`);
504+
}
505+
}
506+
352507
function normalizePath(path) {
353508
let result = String(path || "").replace(/\\/g, "/");
354509
while (result.length > 1 && result.endsWith("/")) {

0 commit comments

Comments
 (0)