i18next-fs-backend
Advanced tools
+8
-0
@@ -0,1 +1,9 @@ | ||
| ### 2.6.4 | ||
| Security release — all issues found via an internal audit. GHSA advisory filed after release. | ||
| - security: refuse to build filesystem paths when `lng` or `ns` values contain `..`, path separators (`/`, `\`), control characters, prototype keys (`__proto__` / `constructor` / `prototype`), or exceed 128 chars. Prevents arbitrary filesystem read / write via attacker-controlled language-code values. Any legitimate i18next language-code shape (BCP-47-like, underscores, hyphens, dots, `+`-joined multi-language requests) is still accepted (GHSA-TBD) | ||
| - docs: new "Security considerations" README section — documents the filesystem-path sanitiser and clarifies the trust model around `.js`/`.ts` locale files (their content is `eval`-ed, so they must be treated as code). The `eval` behaviour itself is retained: dynamic expressions in `.js`/`.ts` locale files are an intentional feature, and safe replacements like `import()` are async-only and not viable for this sync-capable code path. | ||
| - chore: ignore `.env*` and `*.pem`/`*.key` files in `.gitignore`. | ||
| ### 2.6.3 | ||
@@ -2,0 +10,0 @@ |
+11
-3
@@ -55,6 +55,9 @@ "use strict"; | ||
| } | ||
| var filename = (0, _utils.interpolate)(loadPath, { | ||
| var filename = (0, _utils.interpolatePath)(loadPath, { | ||
| lng: language, | ||
| ns: namespace | ||
| }); | ||
| if (filename == null) { | ||
| return callback(new Error('i18next-fs-backend: unsafe lng/ns value — refusing to build filesystem path'), false); | ||
| } | ||
| if (this.allOptions.initAsync === false || this.allOptions.initImmediate === false) { | ||
@@ -124,6 +127,7 @@ try { | ||
| } | ||
| var filename = (0, _utils.interpolate)(addPath, { | ||
| var filename = (0, _utils.interpolatePath)(addPath, { | ||
| lng: language, | ||
| ns: namespace | ||
| }); | ||
| if (filename == null) return; | ||
| (0, _writeFile2.removeFile)(filename, this.options).then(function () {}).catch(function () {}); | ||
@@ -153,6 +157,10 @@ } | ||
| } | ||
| var filename = (0, _utils.interpolate)(addPath, { | ||
| var filename = (0, _utils.interpolatePath)(addPath, { | ||
| lng: lng, | ||
| ns: namespace | ||
| }); | ||
| if (filename == null) { | ||
| (0, _utils.setPath)(this.queuedWrites, [lng, namespace], []); | ||
| return; | ||
| } | ||
| var missings = (0, _utils.getPath)(this.queuedWrites, [lng, namespace]); | ||
@@ -159,0 +167,0 @@ (0, _utils.setPath)(this.queuedWrites, [lng, namespace], []); |
+1
-2
@@ -14,4 +14,3 @@ "use strict"; | ||
| function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } | ||
| function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } | ||
| function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } | ||
| function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } | ||
| var isDeno = typeof Deno !== 'undefined'; | ||
@@ -18,0 +17,0 @@ var isBun = typeof Bun !== 'undefined'; |
+48
-2
@@ -10,11 +10,19 @@ "use strict"; | ||
| exports.interpolate = interpolate; | ||
| exports.interpolatePath = interpolatePath; | ||
| exports.isSafePathSegment = isSafePathSegment; | ||
| exports.pushPath = pushPath; | ||
| exports.setPath = setPath; | ||
| 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; } } }; } | ||
| 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; } } | ||
| 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; } | ||
| var arr = []; | ||
| var each = arr.forEach; | ||
| var slice = arr.slice; | ||
| var UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype']; | ||
| function defaults(obj) { | ||
| each.call(slice.call(arguments, 1), function (source) { | ||
| if (source) { | ||
| for (var prop in source) { | ||
| for (var _i = 0, _Object$keys = Object.keys(source); _i < _Object$keys.length; _i++) { | ||
| var prop = _Object$keys[_i]; | ||
| if (UNSAFE_KEYS.indexOf(prop) > -1) continue; | ||
| if (obj[prop] === undefined) obj[prop] = source[prop]; | ||
@@ -26,2 +34,11 @@ } | ||
| } | ||
| function isSafePathSegment(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; | ||
| if (/[\x00-\x1F\x7F]/.test(v)) return false; | ||
| return true; | ||
| } | ||
| function debounce(func, wait, immediate) { | ||
@@ -84,5 +101,34 @@ var timeout; | ||
| return str.replace(interpolationRegexp, function (match, key) { | ||
| var value = data[key.trim()]; | ||
| var k = key.trim(); | ||
| if (UNSAFE_KEYS.indexOf(k) > -1) return match; | ||
| var value = data[k]; | ||
| return value != null ? value : match; | ||
| }); | ||
| } | ||
| function interpolatePath(str, data) { | ||
| var unsafe = false; | ||
| var result = str.replace(interpolationRegexp, function (match, key) { | ||
| var k = key.trim(); | ||
| if (UNSAFE_KEYS.indexOf(k) > -1) return match; | ||
| var value = data[k]; | ||
| if (value == null) return match; | ||
| var segments = String(value).split('+'); | ||
| var _iterator = _createForOfIteratorHelper(segments), | ||
| _step; | ||
| try { | ||
| for (_iterator.s(); !(_step = _iterator.n()).done;) { | ||
| var seg = _step.value; | ||
| if (!isSafePathSegment(seg)) { | ||
| unsafe = true; | ||
| return match; | ||
| } | ||
| } | ||
| } catch (err) { | ||
| _iterator.e(err); | ||
| } finally { | ||
| _iterator.f(); | ||
| } | ||
| return segments.join('+'); | ||
| }); | ||
| return unsafe ? null : result; | ||
| } |
+1
-2
@@ -15,4 +15,3 @@ "use strict"; | ||
| function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } | ||
| function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } | ||
| function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } | ||
| function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } | ||
| var isDeno = typeof Deno !== 'undefined'; | ||
@@ -19,0 +18,0 @@ var isBun = typeof Bun !== 'undefined'; |
+12
-4
@@ -7,3 +7,3 @@ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } | ||
| function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } | ||
| import { defaults, debounce, getPath, setPath, pushPath, interpolate } from './utils.js'; | ||
| import { defaults, debounce, getPath, setPath, pushPath, interpolatePath } from './utils.js'; | ||
| import { readFile, readFileSync } from './readFile.js'; | ||
@@ -50,6 +50,9 @@ import { writeFile as _writeFile, removeFile as _removeFile } from './writeFile.js'; | ||
| } | ||
| var filename = interpolate(loadPath, { | ||
| var filename = interpolatePath(loadPath, { | ||
| lng: language, | ||
| ns: namespace | ||
| }); | ||
| if (filename == null) { | ||
| return callback(new Error('i18next-fs-backend: unsafe lng/ns value — refusing to build filesystem path'), false); | ||
| } | ||
| if (this.allOptions.initAsync === false || this.allOptions.initImmediate === false) { | ||
@@ -119,6 +122,7 @@ try { | ||
| } | ||
| var filename = interpolate(addPath, { | ||
| var filename = interpolatePath(addPath, { | ||
| lng: language, | ||
| ns: namespace | ||
| }); | ||
| if (filename == null) return; | ||
| _removeFile(filename, this.options).then(function () {}).catch(function () {}); | ||
@@ -148,6 +152,10 @@ } | ||
| } | ||
| var filename = interpolate(addPath, { | ||
| var filename = interpolatePath(addPath, { | ||
| lng: lng, | ||
| ns: namespace | ||
| }); | ||
| if (filename == null) { | ||
| setPath(this.queuedWrites, [lng, namespace], []); | ||
| return; | ||
| } | ||
| var missings = getPath(this.queuedWrites, [lng, namespace]); | ||
@@ -154,0 +162,0 @@ setPath(this.queuedWrites, [lng, namespace], []); |
+46
-2
@@ -0,8 +1,14 @@ | ||
| 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; } } }; } | ||
| 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; } } | ||
| 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; } | ||
| var arr = []; | ||
| var each = arr.forEach; | ||
| var slice = arr.slice; | ||
| var UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype']; | ||
| export function defaults(obj) { | ||
| each.call(slice.call(arguments, 1), function (source) { | ||
| if (source) { | ||
| for (var prop in source) { | ||
| for (var _i = 0, _Object$keys = Object.keys(source); _i < _Object$keys.length; _i++) { | ||
| var prop = _Object$keys[_i]; | ||
| if (UNSAFE_KEYS.indexOf(prop) > -1) continue; | ||
| if (obj[prop] === undefined) obj[prop] = source[prop]; | ||
@@ -14,2 +20,11 @@ } | ||
| } | ||
| export function isSafePathSegment(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; | ||
| if (/[\x00-\x1F\x7F]/.test(v)) return false; | ||
| return true; | ||
| } | ||
| export function debounce(func, wait, immediate) { | ||
@@ -72,5 +87,34 @@ var timeout; | ||
| return str.replace(interpolationRegexp, function (match, key) { | ||
| var value = data[key.trim()]; | ||
| var k = key.trim(); | ||
| if (UNSAFE_KEYS.indexOf(k) > -1) return match; | ||
| var value = data[k]; | ||
| return value != null ? value : match; | ||
| }); | ||
| } | ||
| export function interpolatePath(str, data) { | ||
| var unsafe = false; | ||
| var result = str.replace(interpolationRegexp, function (match, key) { | ||
| var k = key.trim(); | ||
| if (UNSAFE_KEYS.indexOf(k) > -1) return match; | ||
| var value = data[k]; | ||
| if (value == null) return match; | ||
| var segments = String(value).split('+'); | ||
| var _iterator = _createForOfIteratorHelper(segments), | ||
| _step; | ||
| try { | ||
| for (_iterator.s(); !(_step = _iterator.n()).done;) { | ||
| var seg = _step.value; | ||
| if (!isSafePathSegment(seg)) { | ||
| unsafe = true; | ||
| return match; | ||
| } | ||
| } | ||
| } catch (err) { | ||
| _iterator.e(err); | ||
| } finally { | ||
| _iterator.f(); | ||
| } | ||
| return segments.join('+'); | ||
| }); | ||
| return unsafe ? null : result; | ||
| } |
+14
-4
@@ -1,2 +0,2 @@ | ||
| import { defaults, debounce, getPath, setPath, pushPath, interpolate } from './utils.js' | ||
| import { defaults, debounce, getPath, setPath, pushPath, interpolatePath } from './utils.js' | ||
| import { readFile, readFileSync } from './readFile.js' | ||
@@ -38,3 +38,6 @@ import { writeFile, removeFile } from './writeFile.js' | ||
| } | ||
| const filename = interpolate(loadPath, { lng: language, ns: namespace }) | ||
| const filename = interpolatePath(loadPath, { lng: language, ns: namespace }) | ||
| if (filename == null) { | ||
| return callback(new Error('i18next-fs-backend: unsafe lng/ns value — refusing to build filesystem path'), false) | ||
| } | ||
| if (this.allOptions.initAsync === false || this.allOptions.initImmediate === false) { | ||
@@ -102,3 +105,4 @@ try { | ||
| } | ||
| const filename = interpolate(addPath, { lng: language, ns: namespace }) | ||
| const filename = interpolatePath(addPath, { lng: language, ns: namespace }) | ||
| if (filename == null) return | ||
| removeFile(filename, this.options) | ||
@@ -129,3 +133,9 @@ .then(() => {}) | ||
| const filename = interpolate(addPath, { lng, ns: namespace }) | ||
| const filename = interpolatePath(addPath, { lng, ns: namespace }) | ||
| if (filename == null) { | ||
| // drop unsafe queued writes silently — attempting to persist them | ||
| // would either fail or (worse) land in an unexpected filesystem location | ||
| setPath(this.queuedWrites, [lng, namespace], []) | ||
| return | ||
| } | ||
@@ -132,0 +142,0 @@ const missings = getPath(this.queuedWrites, [lng, namespace]) |
+47
-2
@@ -5,6 +5,9 @@ const arr = [] | ||
| const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'] | ||
| export function defaults (obj) { | ||
| each.call(slice.call(arguments, 1), (source) => { | ||
| if (source) { | ||
| for (const prop in source) { | ||
| for (const prop of Object.keys(source)) { | ||
| if (UNSAFE_KEYS.indexOf(prop) > -1) continue | ||
| if (obj[prop] === undefined) obj[prop] = source[prop] | ||
@@ -17,2 +20,19 @@ } | ||
| // Returns true if `v` can be safely interpolated into a filesystem path. | ||
| // Denylist approach — i18next permits arbitrary language-code shapes | ||
| // (https://www.i18next.com/how-to/faq#how-should-the-language-codes-be-formatted) | ||
| // so we only block the concrete attack patterns: path traversal, path | ||
| // separators (so attacker cannot break out of the locale directory), | ||
| // control characters, prototype keys, and oversized inputs. | ||
| export function isSafePathSegment (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 | ||
| } | ||
| export function debounce (func, wait, immediate) { | ||
@@ -77,5 +97,30 @@ let timeout | ||
| return str.replace(interpolationRegexp, (match, key) => { | ||
| const value = data[key.trim()] | ||
| const k = key.trim() | ||
| if (UNSAFE_KEYS.indexOf(k) > -1) return match | ||
| const value = data[k] | ||
| return value != null ? value : match | ||
| }) | ||
| } | ||
| // Path-specific variant: reject values that fail the path-segment safety | ||
| // check. Returns `null` if any substitution is unsafe — callers should bail | ||
| // out cleanly rather than issue the filesystem read/write. For multi-value | ||
| // joins (`en+de`), validates each `+`-separated segment independently. | ||
| export function interpolatePath (str, data) { | ||
| let unsafe = false | ||
| const result = str.replace(interpolationRegexp, (match, key) => { | ||
| const k = key.trim() | ||
| if (UNSAFE_KEYS.indexOf(k) > -1) return match | ||
| const value = data[k] | ||
| if (value == null) return match | ||
| const segments = String(value).split('+') | ||
| for (const seg of segments) { | ||
| if (!isSafePathSegment(seg)) { | ||
| unsafe = true | ||
| return match | ||
| } | ||
| } | ||
| return segments.join('+') | ||
| }) | ||
| return unsafe ? null : result | ||
| } |
+9
-9
| { | ||
| "name": "i18next-fs-backend", | ||
| "version": "2.6.3", | ||
| "version": "2.6.4", | ||
| "private": false, | ||
@@ -31,8 +31,8 @@ "type": "module", | ||
| "devDependencies": { | ||
| "@babel/cli": "7.25.9", | ||
| "@babel/core": "7.26.0", | ||
| "@babel/preset-env": "7.26.0", | ||
| "@babel/cli": "7.28.6", | ||
| "@babel/core": "7.29.0", | ||
| "@babel/preset-env": "7.29.2", | ||
| "babel-plugin-add-module-exports": "1.0.4", | ||
| "dtslint": "4.2.1", | ||
| "esbuild": "0.25.0", | ||
| "esbuild": "0.28.0", | ||
| "eslint": "8.55.0", | ||
@@ -46,10 +46,10 @@ "eslint-config-standard": "17.1.0", | ||
| "expect.js": "0.3.1", | ||
| "i18next": "26.0.3", | ||
| "i18next": "26.0.6", | ||
| "js-yaml": "4.1.1", | ||
| "jsonc-parser": "3.3.1", | ||
| "json5": "2.2.3", | ||
| "mocha": "10.8.2", | ||
| "mocha": "11.7.5", | ||
| "tslint": "5.20.1", | ||
| "tsd": "0.31.2", | ||
| "typescript": "5.6.3", | ||
| "tsd": "0.33.0", | ||
| "typescript": "6.0.3", | ||
| "uglify-js": "3.19.3" | ||
@@ -56,0 +56,0 @@ }, |
+49
-0
@@ -140,2 +140,51 @@ # Introduction | ||
| ## Security considerations | ||
| ### Language / namespace values reach the filesystem | ||
| `i18next-fs-backend` substitutes the `lng` and `ns` options into the | ||
| configured `loadPath` / `addPath` templates and reads / writes the resulting | ||
| file. If those values come from an untrusted source (HTTP query string, | ||
| cookie, request header), a crafted value could break out of the intended | ||
| locale directory and read or overwrite files elsewhere on disk. | ||
| Since **2.6.4**, values containing `..`, `/`, `\`, control characters, | ||
| prototype keys (`__proto__`, `constructor`, `prototype`), or longer than | ||
| 128 characters are rejected — the backend refuses to build the filesystem | ||
| path and returns an error to the caller. Any legitimate i18next | ||
| language-code shape (BCP-47, underscores, dots, `+`-joined multi-language | ||
| requests) is still accepted. | ||
| This is a defence-in-depth layer. It does **not** replace the usual | ||
| responsibility to validate `lng` / `ns` at your application boundary — | ||
| especially when either comes from user input. | ||
| ### `.js` / `.ts` locale files are executed via `eval` | ||
| The backend supports loading translation data from `.js` and `.ts` files by | ||
| `eval`-ing their content. This is an intentional feature — it allows | ||
| expressions, comments, and module-style default exports in locale files — | ||
| but it carries a real trust requirement: | ||
| > **Treat every `.js` / `.ts` locale file as code that will run with the | ||
| > full privileges of your Node process**, including access to | ||
| > `process.env`, the filesystem, and the network. | ||
| Concretely that means: | ||
| - Never load `.js` / `.ts` locale files from an untrusted or writable | ||
| source (user uploads, compromised CDN, shared-mount drops). | ||
| - If your build / deploy pipeline produces locale files, secure the | ||
| pipeline the same way you would secure any code-producing pipeline | ||
| (signed commits, reviewed merges, protected branches). | ||
| - Prefer **JSON / JSON5 / YAML / JSONC** for locale files whenever you | ||
| don't need expression-level dynamism — those formats are parsed, not | ||
| executed. | ||
| ### Reporting a vulnerability | ||
| Please **do not** open a public GitHub issue for security problems. Send | ||
| reports privately via the [GitHub Security Advisories](https://github.com/i18next/i18next-fs-backend/security/advisories/new) | ||
| flow on the repository. | ||
| --- | ||
@@ -142,0 +191,0 @@ |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
675797
1.75%19278
0.86%198
32.89%