CVE-2026-48713
Description
Prototype pollution in i18next-fs-backend ≤2.6.5 via crafted missing-key strings allows arbitrary prototype property writes.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Prototype pollution in i18next-fs-backend ≤2.6.5 via crafted missing-key strings allows arbitrary prototype property writes.
Vulnerability
Versions of i18next-fs-backend prior to 2.6.6 (i.e., ≤2.6.5) are vulnerable to prototype pollution when persisting missing translation keys. The Backend.writeFile() function splits each queued missing-key string on the configured keySeparator (default .) and passes the resulting path segments to the internal setPath() walker (getLastOfPath in lib/utils.js). The walker did not guard against unsafe segments such as __proto__, constructor, or prototype. An attacker can provide a key like __proto__.polluted, which is split into ['__proto__', 'polluted'] and traversed directly into Object.prototype. This attack is only exploitable if the missing-key persistence is exposed to untrusted input (e.g., via i18next-http-middleware's missingKeyHandler with saveMissing: true) and the default key splitting is enabled (keySeparator is not false).
Exploitation
An attacker sends a crafted HTTP request containing a missing-key string such as __proto__.polluted to a route that triggers i18next.t() with saveMissing: true. The i18next-http-middleware's missingKeyHandler queues the string, and Backend.writeFile() splits it on the key separator, producing the path ['__proto__', 'polluted']. The unguarded walker then sets a property on Object.prototype, polluting all objects. No authentication or special privileges are required if the handler is publicly accessible.
Impact
Successful exploitation allows the attacker to write arbitrary properties onto the global Object.prototype. Depending on the host application, this can cause crashes, corrupted translation behavior, configuration poisoning, or bypasses of property-based security checks (e.g., if (user.isAdmin) checks may be subverted). The attack does not directly yield code execution but can severely compromise application logic.
Mitigation
The vulnerability is fixed in i18next-fs-backend version 2.6.6 [1][2]. The traversal helper now refuses to descend through __proto__, constructor, or prototype segments and drops the offending write silently. If upgrading is not immediately possible, administrators should: (1) not expose i18next-http-middleware's missingKeyHandler to untrusted users (mount behind authentication or remove the route), (2) disable missing-key persistence (saveMissing: false), or (3) set keySeparator: false in i18next options (note: this disables nested translation keys).
AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: <2.6.6
Patches
13ab0448087dasecurity: guard setPath/pushPath traversal against prototype pollution
3 files changed · +87 −5
CHANGELOG.md+6 −0 modified@@ -1,3 +1,9 @@ +### 2.6.6 + +Security release — coordinated disclosure from [@codeswhite](https://github.com/codeswhite). See published advisory [GHSA-xxxx-xxxx-xxxx](https://github.com/i18next/i18next-fs-backend/security/advisories/GHSA-xxxx-xxxx-xxxx). + +- security: guard the in-memory `setPath` / `pushPath` traversal (`utils.getLastOfPath`) against prototype pollution via crafted missing-key strings. 2.6.4 sanitised `lng`/`ns` interpolation into filesystem paths, but did not cover the JSON-object walk that `writeFile()` performs on each queued missing-key entry: with the default `keySeparator: '.'`, a key like `__proto__.polluted` was split into `['__proto__','polluted']` and walked straight into `Object.prototype`. The traversal helper now refuses to descend through `__proto__`, `constructor`, or `prototype` segments and drops the offending write silently; legitimate dotted keys (`header.title`) are unaffected. Reachable in practice via `i18next-http-middleware`'s `missingKeyHandler` when exposed to untrusted input — see also the matching defence-in-depth fix in `i18next-http-middleware` 3.9.7. Credit: [@codeswhite](https://github.com/codeswhite) ([GHSA-xxxx-xxxx-xxxx](https://github.com/i18next/i18next-fs-backend/security/advisories/GHSA-xxxx-xxxx-xxxx)). + ### 2.6.5 - fix: allow forward slashes in `ns` values so nested namespace names (mapping to subfolder locale files such as `public/locales/en/a/b.json`) load correctly again. 2.6.4's security fix applied the same strict path-segment check to both `lng` and `ns`, which was correct for `lng` (no BCP-47 shape contains `/`) but over-strict for `ns` — nested namespaces containing `/` were never officially supported, but the behaviour fell out of the implicit string-substitution semantics of `loadPath` and is common enough in the wild to be worth accommodating. `isSafePathSegment` is now split into `isSafeLangSegment` (strict — still rejects `/`) and `isSafeNsSegment` (loose — allows `/` but still rejects `..`, `\`, control chars, prototype keys, and oversized inputs). `isSafePathSegment` is kept as a backwards-compatible alias for the strict check. The 2.6.4 security fix remains in force for every concrete attack pattern from the original advisory. Fixes [#74](https://github.com/i18next/i18next-fs-backend/issues/74).
lib/utils.js+9 −4 modified@@ -91,25 +91,30 @@ function getLastOfPath (object, path, Empty) { if (!object) return {} const key = cleanKey(stack.shift()) + // guard against prototype pollution — refuse to traverse __proto__, + // constructor or prototype segments. Returning an empty result lets + // callers drop the write silently rather than walking into Object.prototype. + if (UNSAFE_KEYS.indexOf(key) > -1) return {} if (!object[key] && Empty) object[key] = new Empty() object = object[key] } if (!object) return {} - return { - obj: object, - k: cleanKey(stack.shift()) - } + const k = cleanKey(stack.shift()) + if (UNSAFE_KEYS.indexOf(k) > -1) return {} + return { obj: object, k } } export function setPath (object, path, newValue) { const { obj, k } = getLastOfPath(object, path, Object) + if (obj === undefined) return // unsafe path — drop silently if (Array.isArray(obj) && isNaN(k)) throw new Error(`Cannot create property "${k}" here since object is an array`) obj[k] = newValue } export function pushPath (object, path, newValue, concat) { const { obj, k } = getLastOfPath(object, path, Object) + if (obj === undefined) return // unsafe path — drop silently obj[k] = obj[k] || [] if (concat) obj[k] = obj[k].concat(newValue) if (!concat) obj[k].push(newValue)
test/security.js+72 −1 modified@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) import i18next from 'i18next' import Backend from '../index.js' -import { isSafeLangSegment, isSafeNsSegment, isSafePathSegment, interpolate, interpolatePath } from '../lib/utils.js' +import { isSafeLangSegment, isSafeNsSegment, isSafePathSegment, interpolate, interpolatePath, setPath, pushPath } from '../lib/utils.js' // Security tests for fixes shipped in the 2.6.x patch release. // See CHANGELOG for associated GHSA advisory. @@ -112,6 +112,77 @@ describe('security', () => { }) }) + describe('setPath / pushPath prototype-pollution guards', () => { + // Reachable via i18next-http-middleware's missingKeyHandler when the + // attacker controls a missing-key string: writeFile() splits the key on + // the configured keySeparator (default `.`), so a key like + // `__proto__.polluted` becomes path `['__proto__','polluted']`, and the + // old getLastOfPath() walked straight into Object.prototype. + + afterEach(() => { + // make sure no test leaks pollution into the rest of the suite + delete Object.prototype.polluted + delete Object.prototype.isAdmin + }) + + it('setPath drops writes whose path traverses __proto__', () => { + const data = {} + setPath(data, ['__proto__', 'polluted'], 'PWNED') + expect(({}).polluted).to.be(undefined) + expect(Object.prototype.polluted).to.be(undefined) + expect(data).to.eql({}) + }) + + it('setPath drops writes whose path traverses constructor / prototype', () => { + const data = {} + setPath(data, ['constructor', 'prototype', 'polluted'], 'PWNED') + setPath(data, ['prototype', 'polluted'], 'PWNED') + expect(({}).polluted).to.be(undefined) + expect(Object.prototype.polluted).to.be(undefined) + expect(data).to.eql({}) + }) + + it('setPath drops a write whose final segment is an unsafe key', () => { + const data = {} + setPath(data, ['en', 'translation', '__proto__'], { isAdmin: true }) + expect(({}).isAdmin).to.be(undefined) + expect(Object.prototype.isAdmin).to.be(undefined) + }) + + it('setPath still writes legitimate nested paths', () => { + const data = {} + setPath(data, ['en', 'translation', 'greeting'], 'hello') + expect(data.en.translation.greeting).to.equal('hello') + }) + + it('setPath handles string paths split by `.` safely', () => { + const data = {} + setPath(data, '__proto__.polluted', 'PWNED') + expect(({}).polluted).to.be(undefined) + expect(data).to.eql({}) + }) + + it('pushPath drops writes whose path traverses unsafe keys', () => { + const data = {} + pushPath(data, ['__proto__', 'polluted'], 'PWNED') + expect(({}).polluted).to.be(undefined) + expect(Object.prototype.polluted).to.be(undefined) + expect(data).to.eql({}) + }) + + it('end-to-end: writeFile()-style key split does not pollute Object.prototype', () => { + // Simulates: missing.key = '__proto__.polluted', keySeparator = '.' + // const path = missing.key.split(keySeparator || '.') + // setPath(data, path, missing.fallbackValue) + const data = {} + const attackerKey = '__proto__.polluted' + const path = attackerKey.split('.') + setPath(data, path, 'PWNED') + expect(({}).polluted).to.be(undefined) + expect(Object.prototype.polluted).to.be(undefined) + }) + }) + describe('Backend.read refuses unsafe lng/ns', () => { let backend before(() => {
Vulnerability mechanics
Root cause
"Missing input validation in getLastOfPath allows prototype pollution via crafted missing-key strings split on keySeparator."
Attack vector
An attacker sends a crafted HTTP request to an endpoint that triggers i18next's missing-key persistence (e.g., i18next-http-middleware's `missingKeyHandler`). The request body includes a missing-key string such as `__proto__.polluted`. `Backend.writeFile()` splits this string on the configured `keySeparator` (default `.`) into the path `['__proto__','polluted']` and passes it to the internal `setPath()` walker (`getLastOfPath` in `lib/utils.js`). Because the walker did not guard against unsafe segments, it traverses into `Object.prototype` and writes the attacker-controlled value onto the global object prototype [ref_id=2][ref_id=1].
What the fix does
The patch modifies `getLastOfPath` in `lib/utils.js` to check each path segment against a list of unsafe keys (`__proto__`, `constructor`, `prototype`) before descending. If an unsafe segment is encountered, the function returns an empty result, causing `setPath` and `pushPath` to silently drop the write instead of walking into `Object.prototype` [patch_id=6112940]. The corresponding early-return guards in `setPath` and `pushPath` (`if (obj === undefined) return`) ensure that no assignment occurs when the traversal was aborted. Legitimate dotted keys like `header.title` continue to work because they do not contain unsafe segments.
Preconditions
- configi18next-fs-backend ≤ 2.6.5 configured as the backend
- authmissingKeyHandler (or equivalent) exposed to untrusted users
- configkeySeparator not set to false (default '.' is used)
- inputAttacker can send crafted missing-key strings (e.g., via HTTP request body)
Generated on Jun 15, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.