fix(common): use null-prototype object for LOCALE_DATA to prevent prototype pollution#68917
fix(common): use null-prototype object for LOCALE_DATA to prevent prototype pollution#68917arturovt wants to merge 1 commit into
Conversation
This means the attacker already has control over code execution. Isn't that the main issue here ? |
|
@JeanMeche You're right that the attacker needs some indirect control over registerLocaleData to exploit this — they can't just call it directly. But the concern is more about availability than code execution. If there's any API endpoint in the app that accepts user input and passes it to registerLocaleData — even indirectly through locale configuration, user preferences, or dynamic i18n loading — a crafted localeId like 'constructor' would corrupt Object.prototype.constructor for the entire Node.js process. Since SSR apps are long-running, that means every user gets broken responses until the server restarts. The Object.create(null) fix is a one-liner that eliminates the risk entirely regardless of how the API is used downstream, so it seems worth having even if the attack surface is narrow. |
…totype pollution
Prior to this commit, `LOCALE_DATA` was initialized as a plain object literal:
```typescript
let LOCALE_DATA: {[localeId: string]: any} = {};
```
While `__proto__` is neutralized by the `replace(/_/g, '-')` sanitization step (becoming `--proto--`), keys like `constructor` and `prototype` pass through unchanged and would modify special properties on `Object.prototype` if used as bracket notation keys on a plain object.
**Example attack through the public API:**
```typescript
// attacker calls the public registerLocaleData API with a crafted localeId
registerLocaleData(data, 'constructor');
// internally becomes:
LOCALE_DATA['constructor'] = data;
// → modifies Object.prototype.constructor for every object in the process
// or with extraData:
registerLocaleData(data, 'constructor', extraData);
// LOCALE_DATA['constructor'][LocaleDataIndex.ExtraData] = extraData;
// → Object.prototype[LocaleDataIndex.ExtraData] = extraData
// → every plain object in the process now has this property
// → affects JSON serialization, property enumeration, and framework internals
// consequence — any subsequent object created in the process is affected:
const user = getUserFromSession();
console.log(user[LocaleDataIndex.ExtraData]); // → attacker-controlled value
```
In a long-running SSR server this pollution persists for the lifetime of the process and affects all subsequent requests from all users.
**The fix** initializes `LOCALE_DATA` with `Object.create(null)`:
```typescript
let LOCALE_DATA: {[localeId: string]: any} = Object.create(null);
```
A null-prototype object has no prototype chain, so any key is treated as a plain string with no special behavior, making prototype pollution impossible regardless of input — without relying on the sanitization step as the sole protection.
Prior to this commit,
LOCALE_DATAwas initialized as a plain object literal:While
__proto__is neutralized by thereplace(/_/g, '-')sanitization step (becoming--proto--), keys likeconstructorandprototypepass through unchanged and would modify special properties onObject.prototypeif used as bracket notation keys on a plain object.Example attack through the public API:
In a long-running SSR server this pollution persists for the lifetime of the process and affects all subsequent requests from all users.
The fix initializes
LOCALE_DATAwithObject.create(null):A null-prototype object has no prototype chain, so any key is treated as a plain string with no special behavior, making prototype pollution impossible regardless of input — without relying on the sanitization step as the sole protection.