Skip to content

Commit b125910

Browse files
committed
fix: address prototype pollution vulnerability (CWE-1321)
1 parent 3c6e52b commit b125910

File tree

3 files changed

+148
-6
lines changed

3 files changed

+148
-6
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "js-toml",
3-
"version": "1.0.1",
3+
"version": "1.0.2",
44
"description": "A TOML parser for JavaScript/TypeScript, targeting TOML 1.0.0 Spec",
55
"keywords": [
66
"toml",

src/load/interpreter.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import { Float } from './tokens/Float.js';
77
import { DateTime } from './tokens/DateTime.js';
88
import { Integer } from './tokens/Integer.js';
99

10-
const isPlainObject = (obj): boolean => obj && obj.constructor === Object;
10+
const isPlainObject = (obj): boolean =>
11+
obj && (obj.constructor === Object || obj.constructor === undefined);
12+
13+
// Create a safe object without prototype pollution vulnerability
14+
const createSafeObject = () => Object.create(null);
1115

1216
const tryCreateKey = (operation, message) => {
1317
try {
@@ -35,7 +39,7 @@ export class Interpreter extends BaseCstVisitor {
3539
}
3640

3741
toml(ctx) {
38-
const root = {};
42+
const root = createSafeObject();
3943
let current = root;
4044
ctx.expression?.forEach(
4145
(expression) => (current = this.visit(expression, { current, root }))
@@ -82,7 +86,8 @@ export class Interpreter extends BaseCstVisitor {
8286
}
8387

8488
inlineTable(ctx) {
85-
const result = { [notEditable]: true };
89+
const result = createSafeObject();
90+
result[notEditable] = true;
8691
if (ctx.inlineTableKeyValues) {
8792
this.visit(ctx.inlineTableKeyValues, result);
8893
}
@@ -141,7 +146,7 @@ export class Interpreter extends BaseCstVisitor {
141146
throw new DuplicateKeyError();
142147
}
143148

144-
const object = {};
149+
const object = createSafeObject();
145150
array.push(object);
146151
return object;
147152
},
@@ -205,7 +210,7 @@ export class Interpreter extends BaseCstVisitor {
205210
throw new DuplicateKeyError();
206211
}
207212
} else {
208-
object[key] = {};
213+
object[key] = createSafeObject();
209214
if (declareSymbol) {
210215
object[key][declareSymbol] = true;
211216
}

test/security.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { load } from '../src/index.js';
3+
4+
describe('Security', () => {
5+
describe('Prototype Pollution Prevention', () => {
6+
it('should not allow __proto__ pollution', () => {
7+
const toml = `
8+
[__proto__]
9+
polluted = true
10+
`;
11+
12+
const result = load(toml);
13+
14+
const testObj = {};
15+
expect('polluted' in testObj).toBe(false);
16+
expect('polluted' in Object.prototype).toBe(false);
17+
18+
expect(result).toHaveProperty('__proto__');
19+
expect(result['__proto__']).toHaveProperty('polluted', true);
20+
});
21+
22+
it('should not allow constructor pollution', () => {
23+
const toml = `
24+
[constructor]
25+
[constructor.prototype]
26+
polluted = true
27+
`;
28+
29+
const result = load(toml);
30+
31+
const testObj = {};
32+
expect('polluted' in testObj).toBe(false);
33+
expect('polluted' in Object.prototype).toBe(false);
34+
35+
expect(result).toHaveProperty('constructor');
36+
});
37+
38+
it('should reproduce the authentication bypass scenario', () => {
39+
const toml = `
40+
[__proto__]
41+
isAdmin = true
42+
`;
43+
44+
// Simulate the vulnerable authentication function
45+
const isAdmin = (user: Record<string, unknown>) => {
46+
return user.isAdmin === true;
47+
};
48+
49+
// Parse the malicious TOML
50+
load(toml);
51+
52+
// Create a user object that should not be admin
53+
const user = { username: 'foo' };
54+
55+
// This should return false (user is not admin)
56+
// If prototype pollution occurred, this would return true
57+
expect(isAdmin(user)).toBe(false);
58+
expect('isAdmin' in user).toBe(false);
59+
});
60+
61+
it('should handle dotted __proto__ keys safely', () => {
62+
const toml = `
63+
[table.__proto__]
64+
polluted = true
65+
`;
66+
67+
const result = load(toml);
68+
69+
const testObj = {};
70+
expect('polluted' in testObj).toBe(false);
71+
expect('polluted' in Object.prototype).toBe(false);
72+
73+
expect('table' in result).toBe(true);
74+
expect('__proto__' in result['table']).toBe(true);
75+
});
76+
77+
it('should handle inline table __proto__ safely', () => {
78+
const toml = `
79+
obj = { __proto__ = { polluted = true } }
80+
`;
81+
82+
const result = load(toml);
83+
84+
const testObj = {};
85+
expect('polluted' in testObj).toBe(false);
86+
expect('polluted' in Object.prototype).toBe(false);
87+
88+
expect(result).toHaveProperty('obj');
89+
});
90+
});
91+
92+
it('should not allow pollution via array of tables syntax', () => {
93+
const toml = `
94+
[[__proto__]]
95+
polluted = true
96+
`;
97+
const result = load(toml);
98+
99+
expect('polluted' in Object.prototype).toBe(false);
100+
expect('polluted' in Array.prototype).toBe(false);
101+
102+
expect(result).toHaveProperty('__proto__');
103+
expect(Array.isArray(result['__proto__'])).toBe(true);
104+
expect(result['__proto__'][0]).toEqual({ polluted: true });
105+
});
106+
107+
it('should handle deeply nested __proto__ keys safely', () => {
108+
const toml = `
109+
[a.b.__proto__.c]
110+
polluted = true
111+
`;
112+
const result = load(toml);
113+
114+
const testObj = {};
115+
expect('polluted' in testObj).toBe(false);
116+
117+
const resultObj = result as Record<
118+
string,
119+
Record<string, Record<string, Record<string, unknown>>>
120+
>;
121+
expect(resultObj.a.b['__proto__'].c).toEqual({ polluted: true });
122+
});
123+
124+
it('should treat "prototype" as a regular key', () => {
125+
const toml = `
126+
[prototype]
127+
polluted = true
128+
`;
129+
const result = load(toml);
130+
131+
const testObj = {};
132+
expect('polluted' in testObj).toBe(false);
133+
134+
expect(result).toHaveProperty('prototype');
135+
expect(result['prototype']).toEqual({ polluted: true });
136+
});
137+
});

0 commit comments

Comments
 (0)