Skip to content

fix(deps): update dependency i18next-http-backend to v4 [security] - #881

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-i18next-http-backend-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-i18next-http-backend-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
i18next-http-backend ^2.1.1 → ^4.0.0 age confidence

i18next-http-backend has Path Traversal & URL Injection via Unsanitised lng/ns

CVE-2026-41691 / GHSA-q89c-q3h5-w34g

More information

Details

Summary

Versions of i18next-http-backend prior to 3.0.5 interpolate the lng and ns values directly into the configured loadPath / addPath URL template without any encoding, validation, or path sanitisation. When an application exposes the language-code selection to user-controlled input (the default — i18next-browser-languagedetector reads ?lng= query params, cookies, localStorage, and request headers), an attacker can inject characters that change the structure of the outgoing request URL.

Affected call sites:

  • _readAny — lib/index.js:64: interpolate(resolvedLoadPath, { lng: languages.join('+'), ns: namespaces.join('+') })
  • create — lib/index.js:123 (pre-patch): interpolate(addPath, { lng, ns: namespace })

The helper interpolate (lib/utils.js) previously returned the raw value with no encoding. In contrast, addQueryString already correctly uses encodeURIComponent for each query-string param — only the URL-path substitution was unprotected.

Impact

An attacker who can influence the resolved lng or ns value can alter the URL in several ways:

  • Path traversal — lng = '../../config' turns /locales//.json into /locales/../../config/translation.json. On a misconfigured web server, this can cause the request to target a different resource than intended; in SSR pipelines that use file:// or similar schemes for loadPath, it can read arbitrary files from the host filesystem.
  • Query-string injection — lng = 'en?admin=true' turns /locales//.json into /locales/en?admin=true/translation.json. Some server frameworks parse the query portion with higher priority than the path and branch on attacker-controlled flags.
  • Fragment truncation — lng = 'en#anything' silently discards the rest of the path in browser fetches (client cannot see the final URL).
  • URL-encoded bypasses — lng = 'en%2F..', after server-side URL decoding, resolves to en/.. — the attacker bypasses the absence of a literal / in their input.

The practical worst case is SSRF when loadPath is an internal or file-scheme URL, and path-based authorisation bypass against servers that segment access by URL prefix.

Also fixed in 3.0.5
  • Per-instance omitFetchOptions. A module-level boolean in lib/request.js was flipped to true the first time any backend instance hit a "not implemented" fetch error. Once flipped, all subsequent requests from all backend instances in the same module silently stripped every user-configured fetch option — including security-relevant credentials, mode, and cache. One misbehaving instance (for example during SSR hydration or in React Native) permanently removed these protections process-wide. 3.0.5 scopes the flag to the backend's options object (options._omitFetchOptions) so one instance's fallback cannot pollute siblings.
  • Log forging via control characters in lng/ns. Error callbacks embedded the raw lng/ns/URL in the message string. Crafted CR/LF values could inject fake log lines into file-backed log aggregators (CWE-117). 3.0.5 strips C0/C1 control chars before concatenation.
  • Basic-auth credentials leaked into error callbacks. If loadPath contained a user:password@host authority, the full URL (including the credentials) ended up in the error message strings returned to the caller. 3.0.5 redacts user:password@ before logging.
  • Prototype-pollution amplification via for...in. addQueryString and the XHR customHeaders loop used for...in which walks the prototype chain. Polluted Object.prototype entries could leak into URL query parameters and request headers. 3.0.5 uses Object.keys and an explicit prototype-key guard.
Affected versions

All versions of i18next-http-backend prior to 3.0.5.

Patch

Fixed in 3.0.5. Summary of the hardening:

  1. New utils.interpolateUrl (used by _readAny and create) returns null if any substitution fails the URL-segment safety check (blocks .., /, \, ?, #, %, @, whitespace, control chars, prototype keys, and values > 128 chars). Multi-language joins (en+de) are validated per-segment. The call sites now refuse to issue a request when the check fails and call back with a clear error.
  2. omitFetchOptions is stored per-instance on options._omitFetchOptions.
  3. Error-callback messages sanitise strings and redact URL credentials.
  4. for...in over untrusted objects replaced with Object.keys + prototype-key guard.
Workarounds

No workaround short of upgrading. If you cannot upgrade immediately, sanitise lng / ns yourself before they reach i18next (strip .., /, \, ?, #, %, whitespace, and control characters; cap the length).

Credits

Discovered via an internal security audit of the i18next ecosystem.

Severity

  • CVSS Score: 6.5 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


i18next-http-backend incomplete URL validation permits SSRF

CVE-2026-105800 / GHSA-xvq9-wjp8-hwqf

More information

Details

There is an SSRF vulnerability when using i18next-http-backend. A colon in an attacker-controlled language value can make a custom path template request an unintended origin.

This is reachable under the following conditions: Attacker controls i18next language or namespace input that is interpolated into loadPath.

Proof of Concept
// SSRF through a colon-only URL scheme in i18next-http-backend interpolation.
const http = require("node:http");
const Backend = require("i18next-http-backend");

function read(backend, lng, ns) {
  return new Promise((resolve) => {
    backend.read(lng, ns, (err) => resolve(err));
  });
}

async function main() {
  let gotRequest = false;
  const server = http.createServer((req, res) => {
    gotRequest = req.url === "/common.json";
    res.writeHead(200, { "content-type": "application/json" });
    res.end("{}");
  });

  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
  const backend = new Backend(null, { loadPath: "/.json" });
  const lng = `http:127.0.0.1:${server.address().port}`;

  const err = await read(backend, lng, "common");
  server.close();

  const vulnerable = gotRequest && !err;
  console.log(vulnerable ? "VULNERABLE" : "SAFE");
  if (!vulnerable) process.exitCode = 1;
}

main();

Run:

npm install --ignore-scripts
node poc.js

The expected result is:

VULNERABLE
Why the Previous Patch Was Incomplete

This vulnerability is caused by an incomplete patch for CVE-2026-41691. The previous patch addressed the following behavior: Original hardening blocks path traversal and selected URL-control characters in lng/ns.

The following bypass remains in the current release: lng=http:127.0.0.1: with loadPath=/.json; colon is not blocked and produces an outbound request.

Impact

The attacker-controlled language or namespace value can redirect the backend request to an unintended origin, resulting in URL injection and possible SSRF.

Recommended Fix

Parse the final URL and require it to remain within the intended origin and path. If absolute URLs are supported, validate them against an explicit allowlist.

Please let us know if you need any additional information or clarification. We are happy to prepare a pull request if that would be helpful. Thank you for reviewing this report.

Maintainer note

Confirmed and fixed in 4.0.2 (commit i18next/i18next-http-backend@07e0288).

Preconditions. The bypass only works when the loadPath / addPath template begins directly with or — no origin and no leading /, e.g. /.json. Only in that position is http: parsed as a URL scheme. The default /locales//.json and every template with a leading path or origin are not affected: a colon inside a path segment has no structural meaning there, and the resulting string is rejected as an invalid URL. The same template shape also allowed an ns value such as //evil.example/x to become a protocol-relative URL in browsers.

Fix. : is now rejected in both lng and ns values, and // in ns values. No BCP-47 language code contains a colon, and : is i18next's default namespace separator, so no usable namespace name does either.

Affected range. Versions before 3.0.5 had no validation at all and are reachable through this vector too, so the affected range is < 4.0.2 rather than >= 3.0.5, <= 4.0.1.

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

i18next/i18next-http-backend (i18next-http-backend)

v4.0.2

Compare Source

Security release. See advisory GHSA-xvq9-wjp8-hwqf.

  • security: also reject : in lng and ns values, and // in ns values. With a loadPath / addPath template that starts with the placeholder (e.g. {{lng}}/{{ns}}.json — no origin and no leading /), a value such as http:127.0.0.1:8080 was interpolated into an absolute URL and the request left the intended origin (SSRF / URL injection). The default /locales/{{lng}}/{{ns}}.json and every template with a leading path or origin were not affected, because a colon inside a path segment has no structural meaning there. No BCP-47 language code contains :, and : is i18next's default nsSeparator, so no usable namespace name does either — the tightened check has no legitimate collateral.

v4.0.1

Compare Source

  • fix: allow @ in ns values so scoped-package-style namespace names such as @organization/package-name fetch correctly again (#​191). 3.0.6 relaxed the ns check for / but left @ in the shared denylist. In the URL path position @ is a plain RFC 3986 pchar with no structural meaning; it stays blocked for lng, where it marks the userinfo/authority boundary. Every other pattern from the 3.0.5 advisory (.., \, %, ?, #, whitespace, control chars, prototype keys, oversized inputs) remains blocked for both keys.

v4.0.0

Compare Source

  • BREAKING: drop cross-fetch dependency. i18next-http-backend now requires a host-provided fetch. This is available in Node ≥ 18 (stable since Node 21), all modern browsers, Deno, and Bun. For runtimes without native fetch, install a ponyfill yourself and inject it via options.alternateFetch, or stay on v3.x.
  • BREAKING: minimum Node version is now 18 (engines.node = ">=18").
  • chore: simplified environment detection in lib/request.js — uses globalThis (with global / window fallbacks for legacy embedded runtimes) instead of separate global.* / window.* branches per API. XHR / ActiveXObject are still picked up if the host provides them, but no longer polyfilled.
  • chore: declared "sideEffects": false for better tree-shaking by downstream bundlers.
  • build: replaced babel + browserify + uglify-js with tsdown (rolldown + oxc). One config produces ESM, CJS, and the IIFE browser bundles. Drops @babel/cli, @babel/core, @babel/preset-env, babel-plugin-add-module-exports, browserify, uglify-js, the fixcjs rewrite hack, and the --ignore cross-fetch browserify flag. Side benefit: minified browser bundle shrinks from ~13 KB to ~6.8 KB (oxc minifier + no babel runtime helpers).
  • build: ESM and CJS outputs are now bundled into a single index.js per format (previously one file per lib/*.js module). The package's exports map is unchanged, so this is invisible to consumers using documented entry points.
  • lint: replaced eslint-config-standard (+ five plugins) with neostandard and migrated to ESLint 9 flat config (eslint.config.mjs). Removed deprecated tslint and dtslint — test:typescript now runs tsc --noEmit plus tsd.
  • chore: tightened .npmignore — published tarball no longer includes the source lib/, the build configs (tsdown.config.ts, eslint.config.mjs, tsconfig.json), or the root index.js re-export shim. Drops from 21.3 KB → ~17 KB packed.
  • docs: alternateFetch is now documented in the README options block as the supported escape hatch for fetch ponyfills, test mocking, and request interception. v4 migration note added to "Getting started".

v3.0.6

Compare Source

  • fix: allow forward slashes in ns values so nested namespace names (mapping to URL layouts such as /locales/en/a/b.json) fetch correctly again. 3.0.5's security fix applied the same strict URL-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. isSafeUrlSegment is now split into isSafeLangUrlSegment (strict — still rejects /) and isSafeNsUrlSegment (loose — allows / but still rejects .., \, URL-structure characters, control chars, prototype keys, and oversized inputs). isSafeUrlSegment is kept as a backwards-compatible alias for the strict check. The 3.0.5 security fix remains in force for every concrete attack pattern from the original advisory.

v3.0.5

Compare Source

Security release — all issues found via an internal audit. See published advisory GHSA-q89c-q3h5-w34g.

  • security: refuse to build request URLs when lng or ns values contain path-traversal, URL-structure (?, #, %, @, whitespace), path separators, control characters, prototype keys, or exceed 128 chars. Prevents path traversal / SSRF / URL injection via attacker-controlled language-code values. isSafeUrlSegment is permissive for legitimate i18next language codes (any BCP-47-like shape, underscores, hyphens, dots, +-joined multi-language requests) (GHSA-q89c-q3h5-w34g)
  • security: per-instance omitFetchOptions — the fetch-options-stripping fallback is now scoped to a single backend instance via options._omitFetchOptions instead of a module-level boolean. One instance hitting a "not implemented" fetch error no longer permanently strips requestOptions (including credentials, mode, cache) from every other backend instance in the same process
  • security: strip CR/LF/NUL and other C0/C1 control characters from lng/ns / URL values before they appear in error-callback strings (CWE-117 log forging)
  • security: redact user:password credentials from URLs before including them in error-callback strings — prevents leaking basic-auth credentials embedded in loadPath / addPath
  • security: iterate own enumerable keys only (Object.keys + prototype-key guard) in addQueryString and in the customHeaders loop in XHR mode — prevents prototype-pollution amplification into the URL and request headers
  • chore: ignore .env* and *.pem/*.key files in .gitignore

v3.0.4

Compare Source

  • use own interpolation function for loadPath and addPath instead of relying on i18next's interpolator i18next#2420 — this means only {{lng}} and {{ns}} placeholders are supported; custom interpolation prefix/suffix from i18next config no longer applies to backend paths

v3.0.2

Compare Source

  • optimize fetchApi selector

v3.0.1

Compare Source

  • try to get rid of top-level await

v3.0.0

Compare Source

  • fix for Deno 2 and removal of unnecessary .cjs file
  • for esm build environments not supporting top-level await, you should import the i18next-http-backend/cjs export or stay at v2.6.2 or v2.7.1

v2.7.3

Compare Source

  • optimize fetchApi selector [backported]

v2.7.2

Compare Source

v2.7.1

Compare Source

  • same as 2.6.2

v2.7.0

Compare Source

  • deprecated, same as v3.0.0

v2.6.2

Compare Source

  • improve network error detection across browsers 152

v2.6.1

Compare Source

  • optimize "Failed to fetch" retry case 147

v2.6.0

Compare Source

  • fix "Failed to fetch" retry case 147

v2.5.2

Compare Source

  • dereference timers in node.js so that the process may exit when finished 139

v2.5.1

Compare Source

  • fix: remove typeof window.document === 'undefined' check which deopt bundle optimization 137

v2.5.0

Compare Source

  • added fetch interceptor to the Backend Options 133

v2.4.3

Compare Source

  • fix: overriding options

v2.4.2

Compare Source

  • fix: mjs typings export

v2.4.1

Compare Source

  • fix: separate cjs and mjs typings

v2.4.0

Compare Source

v2.3.1

Compare Source

  • fix for browser usage

v2.3.0

Compare Source

  • update deps

v2.2.2

Compare Source

  • hack for debug mode in react-native

v2.2.1

Compare Source

  • fix for types moduleResolution "node16"

v2.2.0

Compare Source

  • parseLoadPayload for POST request 110

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@stale

stale Bot commented Oct 3, 2026

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the stale label Oct 3, 2026
@renovate
renovate Bot force-pushed the renovate/npm-i18next-http-backend-vulnerability branch from da4442e to 6c6e657 Compare October 12, 2026 14:36
@renovate renovate Bot changed the title fix(deps): update dependency i18next-http-backend to v3 [security] fix(deps): update dependency i18next-http-backend to v4 [security] Oct 12, 2026
@renovate
renovate Bot force-pushed the renovate/npm-i18next-http-backend-vulnerability branch from 6c6e657 to 94a6fb6 Compare October 14, 2026 21:33
@renovate
renovate Bot force-pushed the renovate/npm-i18next-http-backend-vulnerability branch from 94a6fb6 to d28d0d9 Compare October 20, 2026 11:07

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants