🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

i18next-fs-backend

Package Overview
Dependencies
Maintainers
2
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

i18next-fs-backend - npm Package Compare versions

Comparing version
2.6.3
to
2.6.4
+8
-0
CHANGELOG.md

@@ -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], []);

@@ -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';

@@ -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;
}

@@ -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';

@@ -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], []);

@@ -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;
}

@@ -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])

@@ -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
}
{
"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 @@ },

@@ -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 @@