Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
0e81b26af8 fix(bundler): use Object.create(null) for enum objects to prevent prototype pollution
TypeScript enums were being compiled to use `{}` as the base object,
which inherits from Object.prototype. This caused unexpected behavior
when Object.prototype properties (like numeric indices) had setters
defined, as the enum initialization would trigger those setters.

Changed the enum compilation to use `Object.create(null)` instead of
`{}`, which creates an object with no prototype chain. This prevents
any prototype pollution from affecting enum initialization.

Fixes #24336

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:08:42 +00:00
3 changed files with 143 additions and 4 deletions

View File

@@ -4710,6 +4710,7 @@ pub fn NewParser_(
arg_ref: Ref,
stmts_inside_closure: []Stmt,
all_values_are_pure: bool,
is_enum: bool,
) anyerror!void {
var name_ref = original_name_ref;
@@ -4752,13 +4753,31 @@ pub fn NewParser_(
}
}
// For enums, use Object.create(null) to prevent prototype pollution.
// For namespaces, use {} as before.
const default_object_expr: Expr = if (is_enum) default_object_expr: {
// Object.create(null)
const object_ref = (p.findSymbol(name_loc, "Object") catch unreachable).ref;
const null_args = bun.handleOom(allocator.alloc(Expr, 1));
null_args[0] = p.newExpr(E.Null{}, name_loc);
break :default_object_expr p.newExpr(E.Call{
.target = p.newExpr(E.Dot{
.target = Expr.initIdentifier(object_ref, name_loc),
.name = "create",
.name_loc = name_loc,
}, name_loc),
.args = ExprNodeList.fromOwnedSlice(null_args),
}, name_loc);
} else p.newExpr(E.Object{}, name_loc);
const arg_expr: Expr = arg_expr: {
// TODO: unsupportedJSFeatures.has(.logical_assignment)
// If the "||=" operator is supported, our minified output can be slightly smaller
if (is_export) if (p.enclosing_namespace_arg_ref) |namespace| {
const name = p.symbols.items[name_ref.innerIndex()].original_name;
// "name = (enclosing.name ||= {})"
// "name = (enclosing.name ||= Object.create(null))" for enums
// "name = (enclosing.name ||= {})" for namespaces
p.recordUsage(namespace);
p.recordUsage(name_ref);
break :arg_expr Expr.assign(
@@ -4773,17 +4792,18 @@ pub fn NewParser_(
},
name_loc,
),
.right = p.newExpr(E.Object{}, name_loc),
.right = default_object_expr,
}, name_loc),
);
};
// "name ||= {}"
// "name ||= Object.create(null)" for enums
// "name ||= {}" for namespaces
p.recordUsage(name_ref);
break :arg_expr p.newExpr(E.Binary{
.op = .bin_logical_or_assign,
.left = Expr.initIdentifier(name_ref, name_loc),
.right = p.newExpr(E.Object{}, name_loc),
.right = default_object_expr,
}, name_loc);
};

View File

@@ -1490,6 +1490,7 @@ pub fn VisitStmt(
data.arg,
value_stmts.items,
all_values_are_pure,
true, // is_enum
);
return;
}
@@ -1531,6 +1532,7 @@ pub fn VisitStmt(
data.arg,
prepend_list.items,
false,
false, // is_enum (this is a namespace)
);
return;
}

View File

@@ -0,0 +1,117 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/24336
// TypeScript enum compilation should not trigger Object.prototype setters
// by using Object.create(null) for enum objects instead of {}
describe("issue #24336: enum prototype pollution", () => {
test("enum compilation uses Object.create(null) to prevent prototype pollution", async () => {
using dir = tempDir("24336", {
"enum.ts": `
export enum MyEnum {
A = 0,
B = 1,
C = 2
}
export const value = MyEnum.A;
`,
"index.js": `
// Define a setter on Object.prototype for index 0
let setterCalled = false;
Object.defineProperty(Object.prototype, '0', {
set() {
setterCalled = true;
console.log("SETTER_TRIGGERED");
},
configurable: true
});
// Require the TypeScript file with enum
require('./enum.ts');
// The setter should NOT be triggered since enum uses Object.create(null)
if (setterCalled) {
console.log("FAIL");
process.exit(1);
}
console.log("PASS");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("PASS");
expect(exitCode).toBe(0);
});
test("enum with string value 0 as key also uses Object.create(null)", async () => {
using dir = tempDir("24336-2", {
"enum.ts": `
export enum Direction {
Up = 0,
Down = 1,
Left = 2,
Right = 3
}
`,
"index.js": `
let triggered = false;
Object.defineProperty(Object.prototype, '0', {
set() { triggered = true; },
configurable: true
});
require('./enum.ts');
console.log(triggered ? "FAIL" : "PASS");
process.exit(triggered ? 1 : 0);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("PASS");
expect(exitCode).toBe(0);
});
test("bundler output contains Object.create(null) for enums", async () => {
using dir = tempDir("24336-3", {
"enum.ts": `
export enum Test {
Zero = 0,
One = 1
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "enum.ts", "--no-bundle"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("Object.create(null)");
expect(stdout).not.toContain("||= {}");
expect(exitCode).toBe(0);
});
});