i18next-http-middleware 3.9.6 → 3.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -510,7 +510,7 @@ Don't forget: You have to add the name of your detector (`myDetectorsName` in th
510
510
  <h3 align='center'>Gold Sponsors</h3>
511
511
 
512
512
  <p align='center'>
513
- <a href='https://locize.com/' target='_blank'>
513
+ <a href='https://www.locize.com/?utm_source=i18next_http_middleware_readme&utm_medium=github&utm_campaign=readme' target='_blank'>
514
514
  <img src='https://raw.githubusercontent.com/i18next/i18next/master/assets/locize_sponsor_240.gif' width='240px'>
515
515
  </a>
516
516
  </p>
@@ -93,7 +93,7 @@ var LanguageDetector = function () {
93
93
  return d !== undefined && d !== null && !utils.hasXSS(d);
94
94
  }).map(function (d) {
95
95
  return _this.options.convertDetectedLanguage(d);
96
- });
96
+ }).filter(utils.isSafeLangIdentifier);
97
97
  if (_this.services.languageUtils.getBestMatchFromCodes) {
98
98
  found = _this.services.languageUtils.getBestMatchFromCodes(detections);
99
99
  if (found) {
package/cjs/index.js CHANGED
@@ -280,16 +280,21 @@ function missingKeyHandler(i18next) {
280
280
  res = _checkForCombinedReqR3.res;
281
281
  var lng = options.getParams(req)[options.lngParam || 'lng'];
282
282
  var ns = options.getParams(req)[options.nsParam || 'ns'];
283
+ if (!utils.isSafeLangIdentifier(lng) || !utils.isSafeNsIdentifier(ns)) {
284
+ options.setStatus(res, 400);
285
+ return options.send(res, 'i18next-http-middleware:: unsafe lng/ns value');
286
+ }
283
287
  if (!i18next.services.backendConnector) {
284
288
  options.setStatus(res, 404);
285
289
  return options.send(res, 'i18next-express-middleware:: no backend configured');
286
290
  }
287
291
  var body = options.getBody(req);
292
+ var keySeparator = i18next.options && i18next.options.keySeparator;
288
293
  var saveMissingKeys = function saveMissingKeys(src) {
289
294
  if (!src || _typeof(src) !== 'object') return;
290
295
  for (var _i = 0, _Object$keys = Object.keys(src); _i < _Object$keys.length; _i++) {
291
296
  var m = _Object$keys[_i];
292
- if (utils.UNSAFE_KEYS.indexOf(m) > -1) continue;
297
+ if (utils.hasUnsafeKeySegment(m, keySeparator)) continue;
293
298
  i18next.services.backendConnector.saveMissing([lng], ns, m, src[m]);
294
299
  }
295
300
  };
package/cjs/utils.js CHANGED
@@ -7,6 +7,7 @@ exports.UNSAFE_KEYS = void 0;
7
7
  exports.defaults = defaults;
8
8
  exports.escape = escape;
9
9
  exports.extend = extend;
10
+ exports.hasUnsafeKeySegment = hasUnsafeKeySegment;
10
11
  exports.hasXSS = hasXSS;
11
12
  exports.isSafeIdentifier = void 0;
12
13
  exports.isSafeLangIdentifier = isSafeLangIdentifier;
@@ -14,6 +15,9 @@ exports.isSafeNsIdentifier = isSafeNsIdentifier;
14
15
  exports.removeLngFromUrl = removeLngFromUrl;
15
16
  exports.sanitizeHeaderValue = sanitizeHeaderValue;
16
17
  exports.setPath = setPath;
18
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
19
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
20
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
17
21
  var UNSAFE_KEYS = exports.UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
18
22
  function isSafeIdentifierBase(v) {
19
23
  if (typeof v !== 'string') return false;
@@ -33,6 +37,27 @@ function isSafeNsIdentifier(v) {
33
37
  return isSafeIdentifierBase(v);
34
38
  }
35
39
  var isSafeIdentifier = exports.isSafeIdentifier = isSafeLangIdentifier;
40
+ function hasUnsafeKeySegment(key, keySeparator) {
41
+ if (typeof key !== 'string') return false;
42
+ if (UNSAFE_KEYS.indexOf(key) > -1) return true;
43
+ if (keySeparator === false) return false;
44
+ var sep = keySeparator || '.';
45
+ if (key.indexOf(sep) < 0) return false;
46
+ var segments = key.split(sep);
47
+ var _iterator = _createForOfIteratorHelper(segments),
48
+ _step;
49
+ try {
50
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
51
+ var s = _step.value;
52
+ if (UNSAFE_KEYS.indexOf(s) > -1) return true;
53
+ }
54
+ } catch (err) {
55
+ _iterator.e(err);
56
+ } finally {
57
+ _iterator.f();
58
+ }
59
+ return false;
60
+ }
36
61
  function setPath(object, path, newValue) {
37
62
  var stack;
38
63
  if (typeof path !== 'string') stack = [].concat(path);
@@ -85,7 +85,7 @@ var LanguageDetector = function () {
85
85
  return d !== undefined && d !== null && !utils.hasXSS(d);
86
86
  }).map(function (d) {
87
87
  return _this.options.convertDetectedLanguage(d);
88
- });
88
+ }).filter(utils.isSafeLangIdentifier);
89
89
  if (_this.services.languageUtils.getBestMatchFromCodes) {
90
90
  found = _this.services.languageUtils.getBestMatchFromCodes(detections);
91
91
  if (found) {
package/esm/index.js CHANGED
@@ -263,16 +263,21 @@ export function missingKeyHandler(i18next) {
263
263
  res = _checkForCombinedReqR3.res;
264
264
  var lng = options.getParams(req)[options.lngParam || 'lng'];
265
265
  var ns = options.getParams(req)[options.nsParam || 'ns'];
266
+ if (!utils.isSafeLangIdentifier(lng) || !utils.isSafeNsIdentifier(ns)) {
267
+ options.setStatus(res, 400);
268
+ return options.send(res, 'i18next-http-middleware:: unsafe lng/ns value');
269
+ }
266
270
  if (!i18next.services.backendConnector) {
267
271
  options.setStatus(res, 404);
268
272
  return options.send(res, 'i18next-express-middleware:: no backend configured');
269
273
  }
270
274
  var body = options.getBody(req);
275
+ var keySeparator = i18next.options && i18next.options.keySeparator;
271
276
  var saveMissingKeys = function saveMissingKeys(src) {
272
277
  if (!src || _typeof(src) !== 'object') return;
273
278
  for (var _i = 0, _Object$keys = Object.keys(src); _i < _Object$keys.length; _i++) {
274
279
  var m = _Object$keys[_i];
275
- if (utils.UNSAFE_KEYS.indexOf(m) > -1) continue;
280
+ if (utils.hasUnsafeKeySegment(m, keySeparator)) continue;
276
281
  i18next.services.backendConnector.saveMissing([lng], ns, m, src[m]);
277
282
  }
278
283
  };
package/esm/utils.js CHANGED
@@ -1,3 +1,6 @@
1
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
2
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
3
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
1
4
  export var UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
2
5
  function isSafeIdentifierBase(v) {
3
6
  if (typeof v !== 'string') return false;
@@ -17,6 +20,27 @@ export function isSafeNsIdentifier(v) {
17
20
  return isSafeIdentifierBase(v);
18
21
  }
19
22
  export var isSafeIdentifier = isSafeLangIdentifier;
23
+ export function hasUnsafeKeySegment(key, keySeparator) {
24
+ if (typeof key !== 'string') return false;
25
+ if (UNSAFE_KEYS.indexOf(key) > -1) return true;
26
+ if (keySeparator === false) return false;
27
+ var sep = keySeparator || '.';
28
+ if (key.indexOf(sep) < 0) return false;
29
+ var segments = key.split(sep);
30
+ var _iterator = _createForOfIteratorHelper(segments),
31
+ _step;
32
+ try {
33
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
34
+ var s = _step.value;
35
+ if (UNSAFE_KEYS.indexOf(s) > -1) return true;
36
+ }
37
+ } catch (err) {
38
+ _iterator.e(err);
39
+ } finally {
40
+ _iterator.f();
41
+ }
42
+ return false;
43
+ }
20
44
  export function setPath(object, path, newValue) {
21
45
  var stack;
22
46
  if (typeof path !== 'string') stack = [].concat(path);
@@ -81,9 +81,18 @@ class LanguageDetector {
81
81
  if (!detections) return
82
82
  if (!Array.isArray(detections)) detections = [detections]
83
83
 
84
+ // Detected languages come from untrusted request input (querystring,
85
+ // path, cookie, session, header) and are forwarded to
86
+ // changeLanguage()/loadLanguages(), which hand them to the backend as
87
+ // a `{{lng}}` path/URL segment. hasXSS only covers markup payloads, so
88
+ // the identifier check is what blocks directory escape (`..`, `/`,
89
+ // `\`), control characters and prototype keys. Applied *after*
90
+ // convertDetectedLanguage so a custom converter cannot reintroduce an
91
+ // unsafe value; non-string converter output is dropped (fail-closed).
84
92
  detections = detections
85
93
  .filter((d) => d !== undefined && d !== null && !utils.hasXSS(d))
86
94
  .map((d) => this.options.convertDetectedLanguage(d))
95
+ .filter(utils.isSafeLangIdentifier)
87
96
 
88
97
  if (this.services.languageUtils.getBestMatchFromCodes) { // new i18next v19.5.0
89
98
  found = this.services.languageUtils.getBestMatchFromCodes(detections)
package/lib/index.js CHANGED
@@ -302,6 +302,19 @@ export function missingKeyHandler (i18next, options = {}) {
302
302
  const lng = options.getParams(req)[options.lngParam || 'lng']
303
303
  const ns = options.getParams(req)[options.nsParam || 'ns']
304
304
 
305
+ // Same check getResourcesHandler applies, but this is the write path:
306
+ // these params are forwarded to backendConnector.saveMissing() and end
307
+ // up as the `{{lng}}`/`{{ns}}` segments of the backend's addPath, so an
308
+ // unvalidated value is an arbitrary file write on i18next-fs-backend
309
+ // (route params arrive percent-decoded, e.g. `..%2f..%2ftmp`).
310
+ if (!utils.isSafeLangIdentifier(lng) || !utils.isSafeNsIdentifier(ns)) {
311
+ options.setStatus(res, 400)
312
+ return options.send(
313
+ res,
314
+ 'i18next-http-middleware:: unsafe lng/ns value'
315
+ )
316
+ }
317
+
305
318
  if (!i18next.services.backendConnector) {
306
319
  options.setStatus(res, 404)
307
320
  return options.send(
@@ -311,12 +324,15 @@ export function missingKeyHandler (i18next, options = {}) {
311
324
  }
312
325
 
313
326
  const body = options.getBody(req)
327
+ const keySeparator = i18next.options && i18next.options.keySeparator
314
328
 
315
- // iterate only over own, non-prototype-polluting keys
329
+ // iterate only over own, non-prototype-polluting keys. The check also
330
+ // rejects dotted variants like `__proto__.polluted` whose segments under
331
+ // the configured keySeparator land on an unsafe key — see utils.js.
316
332
  const saveMissingKeys = src => {
317
333
  if (!src || typeof src !== 'object') return
318
334
  for (const m of Object.keys(src)) {
319
- if (utils.UNSAFE_KEYS.indexOf(m) > -1) continue
335
+ if (utils.hasUnsafeKeySegment(m, keySeparator)) continue
320
336
  i18next.services.backendConnector.saveMissing([lng], ns, m, src[m])
321
337
  }
322
338
  }
package/lib/utils.js CHANGED
@@ -43,6 +43,27 @@ export function isSafeNsIdentifier (v) {
43
43
  // exported this symbol; external callers (if any) get the pre-fix behaviour.
44
44
  export const isSafeIdentifier = isSafeLangIdentifier
45
45
 
46
+ // Defense-in-depth check for `missingKeyHandler`. The literal-key denylist
47
+ // catches `{"__proto__": ...}` but misses dotted variants like
48
+ // `{"__proto__.polluted": "x"}` — backends that split the missing-key string
49
+ // on a configured keySeparator (e.g. i18next-fs-backend's writeFile()) would
50
+ // hand that key to an unguarded setPath and walk into Object.prototype.
51
+ // Reject any key whose segments — under the configured keySeparator (default
52
+ // `.`) — include an unsafe key. keySeparator === false means "no splitting",
53
+ // in which case only the literal-key check applies.
54
+ export function hasUnsafeKeySegment (key, keySeparator) {
55
+ if (typeof key !== 'string') return false
56
+ if (UNSAFE_KEYS.indexOf(key) > -1) return true
57
+ if (keySeparator === false) return false
58
+ const sep = keySeparator || '.'
59
+ if (key.indexOf(sep) < 0) return false
60
+ const segments = key.split(sep)
61
+ for (const s of segments) {
62
+ if (UNSAFE_KEYS.indexOf(s) > -1) return true
63
+ }
64
+ return false
65
+ }
66
+
46
67
  export function setPath (object, path, newValue) {
47
68
  let stack
48
69
  if (typeof path !== 'string') stack = [].concat(path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-http-middleware",
3
- "version": "3.9.6",
3
+ "version": "3.9.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./cjs/index.js",
@@ -29,7 +29,7 @@
29
29
  "module": "./esm/index.js",
30
30
  "devDependencies": {
31
31
  "@babel/cli": "7.28.6",
32
- "@babel/core": "7.29.0",
32
+ "@babel/core": "7.29.6",
33
33
  "@babel/preset-env": "7.29.2",
34
34
  "@hapi/hapi": "^21.4.8",
35
35
  "@opentelemetry/api": "^1.9.1",
@@ -66,7 +66,7 @@
66
66
  "homepage": "https://github.com/i18next/i18next-http-middleware",
67
67
  "repository": {
68
68
  "type": "git",
69
- "url": "git@github.com:i18next/i18next-http-middleware.git"
69
+ "url": "https://github.com/i18next/i18next-http-middleware.git"
70
70
  },
71
71
  "bugs": {
72
72
  "url": "https://github.com/i18next/i18next-http-middleware/issues"