Skip to content

fix(common): use null-prototype object for LOCALE_DATA to prevent prototype pollution#68917

Draft
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:fix/locale_data_proto_pollution
Draft

fix(common): use null-prototype object for LOCALE_DATA to prevent prototype pollution#68917
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:fix/locale_data_proto_pollution

Conversation

@arturovt
Copy link
Copy Markdown
Contributor

Prior to this commit, LOCALE_DATA was initialized as a plain object literal:

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:

// 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):

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.

@angular-robot angular-robot Bot added the area: common Issues related to APIs in the @angular/common package label May 25, 2026
@ngbot ngbot Bot added this to the Backlog milestone May 25, 2026
@JeanMeche
Copy link
Copy Markdown
Member

// attacker calls the public registerLocaleData API with a crafted localeId
registerLocaleData(data, 'constructor');

This means the attacker already has control over code execution. Isn't that the main issue here ?

@arturovt
Copy link
Copy Markdown
Contributor Author

@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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: common Issues related to APIs in the @angular/common package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants