Performance and precision pass (#64)

This commit is contained in:
Eli Peter 2026-05-04 19:58:04 -04:00 committed by GitHub
parent c7c5e0f3a1
commit fb698d2c27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 9932 additions and 517 deletions

View file

@ -0,0 +1,60 @@
// Nyx CVE benchmark fixture (patched).
//
// CVE: CVE-2026-42353
// GHSA: GHSA-jfgf-83c5-2c4m
// Project: i18next-http-middleware (i18next/i18next-http-middleware)
// License: MIT (https://github.com/i18next/i18next-http-middleware/blob/master/licence)
// Patched: 65301c194593d46a84623b64e5fde2f51d3550f6 lib/utils.js:1-22, lib/index.js:243-250
// Release: v3.9.3
//
// Patch adds `utils.isSafeIdentifier` (denylist allowing any legitimate
// i18next language code shape, rejecting `..`, path separators, control
// chars, prototype keys, empty strings, and values longer than 128) and
// inserts `languages = languages.filter(utils.isSafeIdentifier)` and the
// equivalent for `namespaces` before they reach the backend connector.
//
// Trims: same scaffolding trims as the vulnerable counterpart.
//
// Patched-form simplification: same template-literal inline of the
// backend's interpolator + readFileSync as the vulnerable side. The
// `utils.isSafeIdentifier` body is copied verbatim from
// `lib/utils.js:13-22` of the patched commit; the prototype-pollution
// denylist (UNSAFE_KEYS check) and length / control-char / `..` /
// separator rejections are all load-bearing for the precision-side
// claim.
const fs = require('fs');
const express = require('express');
const app = express();
const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
function isSafeIdentifier (v) {
if (typeof v !== 'string') return false;
if (v.length === 0 || v.length > 128) return false;
if (UNSAFE_KEYS.indexOf(v) > -1) return false;
if (v.indexOf('..') > -1) return false;
if (v.indexOf('/') > -1 || v.indexOf('\\') > -1) return false;
// eslint-disable-next-line no-control-regex
if (/[\x00-\x1F\x7F]/.test(v)) return false;
return true;
}
app.get('/locales/resources.json', (req, res) => {
let languages = req.query.lng
? req.query.lng.split(' ')
: [];
let namespaces = req.query.ns
? req.query.ns.split(' ')
: [];
// Drop user-supplied values containing patterns that could trigger
// path traversal / SSRF / prototype pollution when forwarded to the
// backend connector. See: https://www.i18next.com/how-to/faq#how-should-the-language-codes-be-formatted
languages = languages.filter(isSafeIdentifier);
namespaces = namespaces.filter(isSafeIdentifier);
const lng = languages[0];
const ns = namespaces[0];
const filename = `/locales/${lng}/${ns}.json`;
fs.readFileSync(filename);
});

View file

@ -0,0 +1,56 @@
// Nyx CVE benchmark fixture.
//
// CVE: CVE-2026-42353
// GHSA: GHSA-jfgf-83c5-2c4m
// Project: i18next-http-middleware (i18next/i18next-http-middleware)
// License: MIT (https://github.com/i18next/i18next-http-middleware/blob/master/licence)
// Advisory: https://github.com/i18next/i18next-http-middleware/security/advisories/GHSA-jfgf-83c5-2c4m
// Vulnerable: a1d92a8f03292644d1c6fa83f1b77121d39daf4d lib/index.js:229-234,246-261
//
// Pre-3.9.3 `getResourcesHandler` pulled `lng` and `ns` directly from
// `options.getQuery(req)` (default: `req => req.query`) and forwarded the
// split values into `i18next.services.backendConnector.load(...)` with no
// sanitisation. Paired with `i18next-fs-backend`, the backend's
// `Backend.read` calls `interpolator.interpolate(loadPath, { lng, ns })`
// which substitutes the unsanitised values into a path template and then
// `readFileSync(filename)`, so a request like
// `GET /locales/resources.json?lng=../../etc/passwd&ns=root` reads
// attacker-chosen files off disk. The advisory also flags the SSRF
// variant when paired with `i18next-http-backend`; we model the
// fs-backend path here because it is the more direct sink-flow shape.
//
// Trims: getResourcesHandler's caching headers (lib/index.js:213-227),
// route-params fallback (L237-244), Response/JSON envelope branch
// (L264-268), the full Backend class wrapper (read/save/create/queue/
// debounce — only the inline interpolation + readFileSync are
// load-bearing), `extendOptionsWithDefaults`, the Backend constructor
// path, `loadPath` typeof-function escape hatch, getResourceBundle
// roundtrip, and the express-router/middleware mount glue.
//
// Patched-form simplification: the upstream interpolator is
// `i18next.services.interpolator.interpolate(loadPath, { lng, ns })`;
// here it is inlined as a template literal because the interpolator
// just substitutes `{{lng}}` and `{{ns}}` placeholders into `loadPath`
// (the default loadPath is `/locales/{{lng}}/{{ns}}.json`). The
// substitution is character-for-character equivalent for the load-
// bearing flow path (lng/ns into the string).
const fs = require('fs');
const express = require('express');
const app = express();
app.get('/locales/resources.json', (req, res) => {
let languages = req.query.lng
? req.query.lng.split(' ')
: [];
let namespaces = req.query.ns
? req.query.ns.split(' ')
: [];
// Inline the backend's read() and forEach loop's body verbatim,
// collapsing the call into the array-index access used by the
// recall test (see disabled_reason in ground_truth.json).
const lng = languages[0];
const ns = namespaces[0];
const filename = `/locales/${lng}/${ns}.json`;
fs.readFileSync(filename);
});