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 +1 -1
- package/cjs/LanguageDetector.js +1 -1
- package/cjs/index.js +6 -1
- package/cjs/utils.js +25 -0
- package/esm/LanguageDetector.js +1 -1
- package/esm/index.js +6 -1
- package/esm/utils.js +24 -0
- package/lib/LanguageDetector.js +9 -0
- package/lib/index.js +18 -2
- package/lib/utils.js +21 -0
- package/package.json +3 -3
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
|
|
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>
|
package/cjs/LanguageDetector.js
CHANGED
|
@@ -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.
|
|
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);
|
package/esm/LanguageDetector.js
CHANGED
|
@@ -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.
|
|
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);
|
package/lib/LanguageDetector.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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"
|