CVE-2026-41691
Description
Copilot said: i18nextify is a JavaScript library that adds i18nextify is a JavaScript library that adds website internationalization via a script tag, without source code changes. Versions prior to 3.0.5 interpolate the lng and ns values directly into the configured loadPath / addPath URL template without any encoding, validation, or path sanitisation. When an application exposes the language-code selection to user-controlled input (the default — i18next-browser-languagedetector reads ?lng= query params, cookies, localStorage, and request headers), an attacker can inject characters that change the structure of the outgoing request URL. This is a single URL-injection vulnerability. The attacker-controlled value is neutralised before it is used as part of an output URL string; the attack shape covers both path traversal and broader URL-structure injection — both are closed by the one interpolateUrl sanitisation fix. This issue has been fixed in version 3.0.5. If users cannot upgrade immediately, they can work around the issue by sanitising lng / ns before they reach i18next (strip .., /, \, ?, #, %, whitespace, and control characters; cap the length).
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
i18next-http-backendnpm | < 3.0.5 | 3.0.5 |
Affected products
1- Range: < 3.0.5
Patches
14cee84f229c6security: hardening for 3.0.5
12 files changed · +363 −44
CHANGELOG.md+11 −0 modified@@ -1,3 +1,14 @@ +### 3.0.5 + +Security release — all issues found via an internal audit. GHSA advisory filed after release. + +- security: refuse to build request URLs when `lng` or `ns` values contain path-traversal, URL-structure (`?`, `#`, `%`, `@`, whitespace), path separators, control characters, prototype keys, or exceed 128 chars. Prevents path traversal / SSRF / URL injection via attacker-controlled language-code values. `isSafeUrlSegment` is permissive for legitimate i18next language codes (any BCP-47-like shape, underscores, hyphens, dots, `+`-joined multi-language requests) (GHSA-TBD) +- security: per-instance `omitFetchOptions` — the fetch-options-stripping fallback is now scoped to a single backend instance via `options._omitFetchOptions` instead of a module-level boolean. One instance hitting a "not implemented" fetch error no longer permanently strips `requestOptions` (including `credentials`, `mode`, `cache`) from every other backend instance in the same process +- security: strip CR/LF/NUL and other C0/C1 control characters from `lng`/`ns` / URL values before they appear in error-callback strings (CWE-117 log forging) +- security: redact `user:password` credentials from URLs before including them in error-callback strings — prevents leaking basic-auth credentials embedded in `loadPath` / `addPath` +- security: iterate own enumerable keys only (`Object.keys` + prototype-key guard) in `addQueryString` and in the `customHeaders` loop in XHR mode — prevents prototype-pollution amplification into the URL and request headers +- chore: ignore `.env*` and `*.pem`/`*.key` files in `.gitignore` + ### 3.0.4 - use own interpolation function for loadPath and addPath instead of relying on i18next's interpolator [i18next#2420](https://github.com/i18next/i18next/issues/2420) — this means only `{{lng}}` and `{{ns}}` placeholders are supported; custom interpolation prefix/suffix from i18next config no longer applies to backend paths
.github/workflows/deno.yml+2 −2 modified@@ -11,13 +11,13 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - deno: [ '2.x', '1.x' ] + deno: [ '2.x' ] # os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest] steps: - uses: actions/checkout@v4 - name: Setup Deno - uses: denolib/setup-deno@master + uses: denolib/setup-deno@v2 with: deno-version: ${{ matrix.deno }} - run: deno --version
.github/workflows/node.yml+2 −2 modified@@ -15,9 +15,9 @@ jobs: # os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: npm install
.gitignore+8 −1 modified@@ -3,4 +3,11 @@ node_modules package-lock.json cjs esm -deno.lock \ No newline at end of file +deno.lock + +# Secrets & credentials +.env +.env.* +!.env.example +*.pem +*.key
i18nextHttpBackend.js+96 −15 modified@@ -93,10 +93,15 @@ var Backend = function () { loadPath = (0, _utils.makePromise)(loadPath); loadPath.then(function (resolvedLoadPath) { if (!resolvedLoadPath) return callback(null, {}); - var url = (0, _utils.interpolate)(resolvedLoadPath, { + var url = (0, _utils.interpolateUrl)(resolvedLoadPath, { lng: languages.join('+'), ns: namespaces.join('+') }); + if (url == null) { + var safeLngs = languages.map(_utils.sanitizeLogValue).join(', '); + var safeNss = namespaces.map(_utils.sanitizeLogValue).join(', '); + return callback(new Error('i18next-http-backend: unsafe lng/ns value — refusing to build request URL for languages=[' + safeLngs + '] namespaces=[' + safeNss + ']'), false); + } _this2.loadUrl(url, callback, loadUrlLanguages, loadUrlNamespaces); }); } @@ -107,16 +112,17 @@ var Backend = function () { var lng = typeof languages === 'string' ? [languages] : languages; var ns = typeof namespaces === 'string' ? [namespaces] : namespaces; var payload = this.options.parseLoadPayload(lng, ns); + var safeUrl = (0, _utils.sanitizeLogValue)((0, _utils.redactUrlCredentials)(url)); this.options.request(this.options, url, payload, function (err, res) { - if (res && (res.status >= 500 && res.status < 600 || !res.status)) return callback('failed loading ' + url + '; status code: ' + res.status, true); - if (res && res.status >= 400 && res.status < 500) return callback('failed loading ' + url + '; status code: ' + res.status, false); + if (res && (res.status >= 500 && res.status < 600 || !res.status)) return callback('failed loading ' + safeUrl + '; status code: ' + res.status, true); + if (res && res.status >= 400 && res.status < 500) return callback('failed loading ' + safeUrl + '; status code: ' + res.status, false); if (!res && err && err.message) { var errorMessage = err.message.toLowerCase(); var isNetworkError = ['failed', 'fetch', 'network', 'load'].find(function (term) { return errorMessage.indexOf(term) > -1; }); if (isNetworkError) { - return callback('failed loading ' + url + ': ' + err.message, true); + return callback('failed loading ' + safeUrl + ': ' + (0, _utils.sanitizeLogValue)(err.message), true); } } if (err) return callback(err, false); @@ -128,7 +134,7 @@ var Backend = function () { ret = res.data; } } catch (e) { - parseErr = 'failed parsing ' + url + ' to json'; + parseErr = 'failed parsing ' + safeUrl + ' to json'; } if (parseErr) return callback(parseErr, false); callback(null, ret); @@ -149,10 +155,15 @@ var Backend = function () { if (typeof _this4.options.addPath === 'function') { addPath = _this4.options.addPath(lng, namespace); } - var url = (0, _utils.interpolate)(addPath, { + var url = (0, _utils.interpolateUrl)(addPath, { lng: lng, ns: namespace }); + if (url == null) { + finished += 1; + if (callback && finished === languages.length) callback(dataArray, resArray); + return; + } _this4.options.request(_this4.options, url, payload, function (data, res) { finished += 1; dataArray.push(data); @@ -214,8 +225,7 @@ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } 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); } 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 fetchApi = typeof fetch === 'function' ? fetch : undefined; if (typeof global !== 'undefined' && global.fetch) { fetchApi = global.fetch; @@ -244,10 +254,13 @@ if (!fetchApi && !XmlHttpRequestApi && !ActiveXObjectApi) { fetchApi = require('cross-fetch'); } catch (e) {} } +var UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype']; var addQueryString = function addQueryString(url, params) { if (params && _typeof(params) === 'object') { var queryString = ''; - for (var paramName in params) { + for (var _i = 0, _Object$keys = Object.keys(params); _i < _Object$keys.length; _i++) { + var paramName = _Object$keys[_i]; + if (UNSAFE_KEYS.indexOf(paramName) > -1) continue; queryString += '&' + encodeURIComponent(paramName) + '=' + encodeURIComponent(params[paramName]); } if (!queryString) return url; @@ -280,7 +293,6 @@ var fetchIt = function fetchIt(url, fetchOptions, callback, altFetch) { fetchApi(url, fetchOptions).then(resolver).catch(callback); } }; -var omitFetchOptions = false; var requestWithFetch = function requestWithFetch(options, url, payload, callback) { if (options.queryStringParams) { url = addQueryString(url, options.queryStringParams); @@ -295,7 +307,7 @@ var requestWithFetch = function requestWithFetch(options, url, payload, callback method: payload ? 'POST' : 'GET', body: payload ? options.stringify(payload) : undefined, headers: headers - }, omitFetchOptions ? {} : reqOptions); + }, options._omitFetchOptions ? {} : reqOptions); var altFetch = typeof options.alternateFetch === 'function' && options.alternateFetch.length >= 1 ? options.alternateFetch : undefined; try { fetchIt(url, fetchOptions, callback, altFetch); @@ -308,7 +320,7 @@ var requestWithFetch = function requestWithFetch(options, url, payload, callback delete fetchOptions[opt]; }); fetchIt(url, fetchOptions, callback, altFetch); - omitFetchOptions = true; + options._omitFetchOptions = true; } catch (err) { callback(err); } @@ -337,7 +349,9 @@ var requestWithXmlHttpRequest = function requestWithXmlHttpRequest(options, url, var h = options.customHeaders; h = typeof h === 'function' ? h() : h; if (h) { - for (var i in h) { + for (var _i2 = 0, _Object$keys2 = Object.keys(h); _i2 < _Object$keys2.length; _i2++) { + var i = _Object$keys2[_i2]; + if (UNSAFE_KEYS.indexOf(i) > -1) continue; x.setRequestHeader(i, h[i]); } } @@ -378,21 +392,59 @@ Object.defineProperty(exports, "__esModule", { exports.defaults = defaults; exports.hasXMLHttpRequest = hasXMLHttpRequest; exports.interpolate = interpolate; +exports.interpolateUrl = interpolateUrl; +exports.isSafeUrlSegment = isSafeUrlSegment; exports.makePromise = makePromise; +exports.redactUrlCredentials = redactUrlCredentials; +exports.sanitizeLogValue = sanitizeLogValue; +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; } 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); } 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]; } } }); return obj; } +function isSafeUrlSegment(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 (/[?#%\s@]/.test(v)) return false; + if (/[\x00-\x1F\x7F]/.test(v)) return false; + return true; +} +function sanitizeLogValue(v) { + if (typeof v !== 'string') return v; + return v.replace(/[\r\n\x00-\x1F\x7F]/g, ' '); +} +function redactUrlCredentials(u) { + if (typeof u !== 'string' || u.length === 0) return u; + try { + var parsed = new URL(u); + if (parsed.username || parsed.password) { + parsed.username = ''; + parsed.password = ''; + return parsed.toString(); + } + return u; + } catch (e) { + return u.replace(/(\/\/)[^/@\s]+@/g, '$1'); + } +} function hasXMLHttpRequest() { return typeof XMLHttpRequest === 'function' || (typeof XMLHttpRequest === "undefined" ? "undefined" : _typeof(XMLHttpRequest)) === 'object'; } @@ -408,10 +460,39 @@ function makePromise(maybePromise) { var interpolationRegexp = /\{\{(.+?)\}\}/g; function interpolate(str, data) { 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 interpolateUrl(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 (!isSafeUrlSegment(seg)) { + unsafe = true; + return match; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + return segments.join('+'); + }); + return unsafe ? null : result; +} },{}],4:[function(require,module,exports){ },{}]},{},[1])(1)
i18nextHttpBackend.min.js+1 −1 modified@@ -1 +1 @@ -!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).i18nextHttpBackend=e()}(function(){return function o(r,i,a){function s(t,e){if(!i[t]){if(!r[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}n=i[t]={exports:{}},r[t][0].call(n.exports,function(e){return s(r[t][1][e]||e)},n,n.exports,o,r,i,a)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;e<a.length;e++)s(a[e]);return s}({1:[function(e,t,n){Object.defineProperty(n,"__esModule",{value:!0}),n.default=void 0;var f=e("./utils.js"),r=(e=e("./request.js"))&&e.__esModule?e:{default:e};function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(t,e){var n,o=Object.keys(t);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(t),e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),o.push.apply(o,n)),o}function a(t){for(var e=1;e<arguments.length;e++){var n=null!=arguments[e]?arguments[e]:{};e%2?o(Object(n),!0).forEach(function(e){u(t,e,n[e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))})}return t}function s(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,c(o.key),o)}}function u(e,t,n){return(t=c(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function c(e){e=((e,t)=>{if("object"!=i(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0===n)return("string"===t?String:Number)(e);if("object"!=i(n=n.call(e,t||"default")))return n;throw new TypeError("@@toPrimitive must return a primitive value.")})(e,"string");return"symbol"==i(e)?e:e+""}e=function e(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},r=this,i=e;if(!(r instanceof i))throw new TypeError("Cannot call a class as a function");this.services=t,this.options=n,this.allOptions=o,this.type="backend",this.init(t,n,o)},(d=[{key:"init",value:function(e){var t=this,n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};this.services=e,this.options=a(a(a({},{loadPath:"/locales/{{lng}}/{{ns}}.json",addPath:"/locales/add/{{lng}}/{{ns}}",parse:function(e){return JSON.parse(e)},stringify:JSON.stringify,parsePayload:function(e,t,n){return u({},t,n||"")},parseLoadPayload:function(e,t){},request:r.default,reloadInterval:"undefined"==typeof window&&36e5,customHeaders:{},queryStringParams:{},crossDomain:!1,withCredentials:!1,overrideMimeType:!1,requestOptions:{mode:"cors",credentials:"same-origin",cache:"default"}}),this.options||{}),n),this.allOptions=o,this.services&&this.options.reloadInterval&&"object"===i(e=setInterval(function(){return t.reload()},this.options.reloadInterval))&&"function"==typeof e.unref&&e.unref()}},{key:"readMulti",value:function(e,t,n){this._readAny(e,e,t,t,n)}},{key:"read",value:function(e,t,n){this._readAny([e],e,[t],t,n)}},{key:"_readAny",value:function(t,n,o,r,i){var a=this,e=this.options.loadPath;"function"==typeof this.options.loadPath&&(e=this.options.loadPath(t,o)),(e=(0,f.makePromise)(e)).then(function(e){if(!e)return i(null,{});e=(0,f.interpolate)(e,{lng:t.join("+"),ns:o.join("+")});a.loadUrl(e,i,n,r)})}},{key:"loadUrl",value:function(i,a,s,u){var c=this,e=this.options.parseLoadPayload("string"==typeof s?[s]:s,"string"==typeof u?[u]:u);this.options.request(this.options,i,e,function(e,t){if(t&&(500<=t.status&&t.status<600||!t.status))return a("failed loading "+i+"; status code: "+t.status,!0);if(t&&400<=t.status&&t.status<500)return a("failed loading "+i+"; status code: "+t.status,!1);if(!t&&e&&e.message){var n=e.message.toLowerCase();if(["failed","fetch","network","load"].find(function(e){return-1<n.indexOf(e)}))return a("failed loading "+i+": "+e.message,!0)}if(e)return a(e,!1);var o,r;try{o="string"==typeof t.data?c.options.parse(t.data,s,u):t.data}catch(e){r="failed parsing "+i+" to json"}if(r)return a(r,!1);a(null,o)})}},{key:"create",value:function(n,o,e,t,r){var i,a,s,u,c=this;this.options.addPath&&("string"==typeof n&&(n=[n]),i=this.options.parsePayload(o,e,t),a=0,s=[],u=[],n.forEach(function(e){var t=c.options.addPath,t=("function"==typeof c.options.addPath&&(t=c.options.addPath(e,o)),(0,f.interpolate)(t,{lng:e,ns:o}));c.options.request(c.options,t,i,function(e,t){a+=1,s.push(e),u.push(t),a===n.length&&"function"==typeof r&&r(s,u)})}))}},{key:"reload",value:function(){var t,e,n=this,o=this.services,r=o.backendConnector,i=o.languageUtils,a=o.logger,o=r.language;o&&"cimode"===o.toLowerCase()||(t=[],(e=function(e){i.toResolveHierarchy(e).forEach(function(e){t.indexOf(e)<0&&t.push(e)})})(o),this.allOptions.preload&&this.allOptions.preload.forEach(e),t.forEach(function(o){n.allOptions.ns.forEach(function(n){r.read(o,n,"read",null,null,function(e,t){e&&a.warn("loading namespace ".concat(n," for language ").concat(o," failed"),e),!e&&t&&a.log("loaded namespace ".concat(n," for language ").concat(o),t),r.loaded("".concat(o,"|").concat(n),e,t)})})}))}}])&&s(e.prototype,d),l&&s(e,l),Object.defineProperty(e,"prototype",{writable:!1});var l,d=e;d.type="backend",n.default=d;t.exports=n.default},{"./request.js":2,"./utils.js":3}],2:[function(e,n,o){!function(S){!function(){Object.defineProperty(o,"__esModule",{value:!0}),o.default=void 0;var h=e("./utils.js");function t(t,e){var n,o=Object.keys(t);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(t),e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),o.push.apply(o,n)),o}function b(o){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?t(Object(r),!0).forEach(function(e){var t,n;t=o,n=r[e=e],(e=(e=>(e=((e,t)=>{if("object"!=v(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0===n)return("string"===t?String:Number)(e);if("object"!=v(n=n.call(e,t||"default")))return n;throw new TypeError("@@toPrimitive must return a primitive value.")})(e,"string"),"symbol"==v(e)?e:e+""))(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(r)):t(Object(r)).forEach(function(e){Object.defineProperty(o,e,Object.getOwnPropertyDescriptor(r,e))})}return o}function v(e){return(v="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var m,g,O="function"==typeof fetch?fetch:void 0;if(void 0!==S&&S.fetch?O=S.fetch:"undefined"!=typeof window&&window.fetch&&(O=window.fetch),(0,h.hasXMLHttpRequest)()&&(void 0!==S&&S.XMLHttpRequest?m=S.XMLHttpRequest:"undefined"!=typeof window&&window.XMLHttpRequest&&(m=window.XMLHttpRequest)),"function"==typeof ActiveXObject&&(void 0!==S&&S.ActiveXObject?g=S.ActiveXObject:"undefined"!=typeof window&&window.ActiveXObject&&(g=window.ActiveXObject)),!(O="function"!=typeof O?void 0:O)&&!m&&!g)try{O=e("cross-fetch")}catch(e){}var w=function(e,t){if(t&&"object"===v(t)){var n,o="";for(n in t)o+="&"+encodeURIComponent(n)+"="+encodeURIComponent(t[n]);if(!o)return e;e=e+(-1!==e.indexOf("?")?"&":"?")+o.slice(1)}return e},j=function(e,t,n,o){function r(t){if(!t.ok)return n(t.statusText||"Error",{status:t.status});t.text().then(function(e){n(null,{status:t.status,data:e})}).catch(n)}if(o){o=o(e,t);if(o instanceof Promise)return void o.then(r).catch(n)}("function"==typeof fetch?fetch:O)(e,t).then(r).catch(n)},P=!1;o.default=function(e,t,n,o){if("function"==typeof n&&(o=n,n=void 0),o=o||function(){},O&&0!==t.indexOf("file:")){var r=e,i=t,a=n,s=o,u=(r.queryStringParams&&(i=w(i,r.queryStringParams)),b({},"function"==typeof r.customHeaders?r.customHeaders():r.customHeaders)),c=("undefined"==typeof window&&void 0!==S&&void 0!==S.process&&S.process.versions&&S.process.versions.node&&(u["User-Agent"]="i18next-http-backend (node/".concat(S.process.version,"; ").concat(S.process.platform," ").concat(S.process.arch,")")),a&&(u["Content-Type"]="application/json"),"function"==typeof r.requestOptions?r.requestOptions(a):r.requestOptions),f=b({method:a?"POST":"GET",body:a?r.stringify(a):void 0,headers:u},P?{}:c),a="function"==typeof r.alternateFetch&&1<=r.alternateFetch.length?r.alternateFetch:void 0;try{j(i,f,s,a)}catch(e){if(!c||0===Object.keys(c).length||!e.message||e.message.indexOf("not implemented")<0)return s(e);try{Object.keys(c).forEach(function(e){delete f[e]}),j(i,f,s,a),P=!0}catch(e){s(e)}}}else if((0,h.hasXMLHttpRequest)()||"function"==typeof ActiveXObject){var u=e,r=t,c=n,l=o;c&&"object"===v(c)&&(c=w("",c).slice(1)),u.queryStringParams&&(r=w(r,u.queryStringParams));try{var d=m?new m:new g("MSXML2.XMLHTTP.3.0"),p=(d.open(c?"POST":"GET",r,1),u.crossDomain||d.setRequestHeader("X-Requested-With","XMLHttpRequest"),d.withCredentials=!!u.withCredentials,c&&d.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),d.overrideMimeType&&d.overrideMimeType("application/json"),u.customHeaders);if(p="function"==typeof p?p():p)for(var y in p)d.setRequestHeader(y,p[y]);d.onreadystatechange=function(){3<d.readyState&&l(400<=d.status?d.statusText:null,{status:d.status,data:d.responseText})},d.send(c)}catch(e){console&&console.log(e)}}else o(new Error("No fetch and no xhr implementation found!"))};n.exports=o.default}.call(this)}.call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./utils.js":3,"cross-fetch":4}],3:[function(e,t,n){function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}Object.defineProperty(n,"__esModule",{value:!0}),n.defaults=function(n){return r.call(i.call(arguments,1),function(e){if(e)for(var t in e)void 0===n[t]&&(n[t]=e[t])}),n},n.hasXMLHttpRequest=function(){return"function"==typeof XMLHttpRequest||"object"===("undefined"==typeof XMLHttpRequest?"undefined":o(XMLHttpRequest))},n.interpolate=function(e,n){return e.replace(a,function(e,t){t=n[t.trim()];return null!=t?t:e})},n.makePromise=function(e){if((e=>e&&"function"==typeof e.then)(e))return e;return Promise.resolve(e)};var n=[],r=n.forEach,i=n.slice;var a=/\{\{(.+?)\}\}/g},{}],4:[function(e,t,n){},{}]},{},[1])(1)}); \ No newline at end of file +!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).i18nextHttpBackend=e()}(function(){return function o(r,i,a){function s(t,e){if(!i[t]){if(!r[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}n=i[t]={exports:{}},r[t][0].call(n.exports,function(e){return s(r[t][1][e]||e)},n,n.exports,o,r,i,a)}return i[t].exports}for(var u="function"==typeof require&&require,e=0;e<a.length;e++)s(a[e]);return s}({1:[function(e,t,n){Object.defineProperty(n,"__esModule",{value:!0}),n.default=void 0;var f=e("./utils.js"),r=(e=e("./request.js"))&&e.__esModule?e:{default:e};function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(t,e){var n,o=Object.keys(t);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(t),e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),o.push.apply(o,n)),o}function a(t){for(var e=1;e<arguments.length;e++){var n=null!=arguments[e]?arguments[e]:{};e%2?o(Object(n),!0).forEach(function(e){u(t,e,n[e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach(function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))})}return t}function s(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,c(o.key),o)}}function u(e,t,n){return(t=c(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function c(e){e=((e,t)=>{if("object"!=i(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0===n)return("string"===t?String:Number)(e);if("object"!=i(n=n.call(e,t||"default")))return n;throw new TypeError("@@toPrimitive must return a primitive value.")})(e,"string");return"symbol"==i(e)?e:e+""}e=function e(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},r=this,i=e;if(!(r instanceof i))throw new TypeError("Cannot call a class as a function");this.services=t,this.options=n,this.allOptions=o,this.type="backend",this.init(t,n,o)},(d=[{key:"init",value:function(e){var t=this,n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};this.services=e,this.options=a(a(a({},{loadPath:"/locales/{{lng}}/{{ns}}.json",addPath:"/locales/add/{{lng}}/{{ns}}",parse:function(e){return JSON.parse(e)},stringify:JSON.stringify,parsePayload:function(e,t,n){return u({},t,n||"")},parseLoadPayload:function(e,t){},request:r.default,reloadInterval:"undefined"==typeof window&&36e5,customHeaders:{},queryStringParams:{},crossDomain:!1,withCredentials:!1,overrideMimeType:!1,requestOptions:{mode:"cors",credentials:"same-origin",cache:"default"}}),this.options||{}),n),this.allOptions=o,this.services&&this.options.reloadInterval&&"object"===i(e=setInterval(function(){return t.reload()},this.options.reloadInterval))&&"function"==typeof e.unref&&e.unref()}},{key:"readMulti",value:function(e,t,n){this._readAny(e,e,t,t,n)}},{key:"read",value:function(e,t,n){this._readAny([e],e,[t],t,n)}},{key:"_readAny",value:function(o,r,i,a,s){var u=this,e=this.options.loadPath;"function"==typeof this.options.loadPath&&(e=this.options.loadPath(o,i)),(e=(0,f.makePromise)(e)).then(function(e){var t,n;return e?null==(e=(0,f.interpolateUrl)(e,{lng:o.join("+"),ns:i.join("+")}))?(t=o.map(f.sanitizeLogValue).join(", "),n=i.map(f.sanitizeLogValue).join(", "),s(new Error("i18next-http-backend: unsafe lng/ns value — refusing to build request URL for languages=["+t+"] namespaces=["+n+"]"),!1)):void u.loadUrl(e,s,r,a):s(null,{})})}},{key:"loadUrl",value:function(e,i,a,s){var u=this,t=this.options.parseLoadPayload("string"==typeof a?[a]:a,"string"==typeof s?[s]:s),c=(0,f.sanitizeLogValue)((0,f.redactUrlCredentials)(e));this.options.request(this.options,e,t,function(e,t){if(t&&(500<=t.status&&t.status<600||!t.status))return i("failed loading "+c+"; status code: "+t.status,!0);if(t&&400<=t.status&&t.status<500)return i("failed loading "+c+"; status code: "+t.status,!1);if(!t&&e&&e.message){var n=e.message.toLowerCase();if(["failed","fetch","network","load"].find(function(e){return-1<n.indexOf(e)}))return i("failed loading "+c+": "+(0,f.sanitizeLogValue)(e.message),!0)}if(e)return i(e,!1);var o,r;try{o="string"==typeof t.data?u.options.parse(t.data,a,s):t.data}catch(e){r="failed parsing "+c+" to json"}if(r)return i(r,!1);i(null,o)})}},{key:"create",value:function(n,o,e,t,r){var i,a,s,u,c=this;this.options.addPath&&("string"==typeof n&&(n=[n]),i=this.options.parsePayload(o,e,t),a=0,s=[],u=[],n.forEach(function(e){var t=c.options.addPath,t=("function"==typeof c.options.addPath&&(t=c.options.addPath(e,o)),(0,f.interpolateUrl)(t,{lng:e,ns:o}));null==t?(a+=1,r&&a===n.length&&r(s,u)):c.options.request(c.options,t,i,function(e,t){a+=1,s.push(e),u.push(t),a===n.length&&"function"==typeof r&&r(s,u)})}))}},{key:"reload",value:function(){var t,e,n=this,o=this.services,r=o.backendConnector,i=o.languageUtils,a=o.logger,o=r.language;o&&"cimode"===o.toLowerCase()||(t=[],(e=function(e){i.toResolveHierarchy(e).forEach(function(e){t.indexOf(e)<0&&t.push(e)})})(o),this.allOptions.preload&&this.allOptions.preload.forEach(e),t.forEach(function(o){n.allOptions.ns.forEach(function(n){r.read(o,n,"read",null,null,function(e,t){e&&a.warn("loading namespace ".concat(n," for language ").concat(o," failed"),e),!e&&t&&a.log("loaded namespace ".concat(n," for language ").concat(o),t),r.loaded("".concat(o,"|").concat(n),e,t)})})}))}}])&&s(e.prototype,d),l&&s(e,l),Object.defineProperty(e,"prototype",{writable:!1});var l,d=e;d.type="backend",n.default=d;t.exports=n.default},{"./request.js":2,"./utils.js":3}],2:[function(e,n,o){!function(q){!function(){Object.defineProperty(o,"__esModule",{value:!0}),o.default=void 0;var b=e("./utils.js");function t(t,e){var n,o=Object.keys(t);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(t),e&&(n=n.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),o.push.apply(o,n)),o}function m(o){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?t(Object(r),!0).forEach(function(e){var t,n;t=o,n=r[e=e],(e=(e=>(e=((e,t)=>{if("object"!=g(e)||!e)return e;var n=e[Symbol.toPrimitive];if(void 0===n)return("string"===t?String:Number)(e);if("object"!=g(n=n.call(e,t||"default")))return n;throw new TypeError("@@toPrimitive must return a primitive value.")})(e,"string"),"symbol"==g(e)?e:e+""))(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(r)):t(Object(r)).forEach(function(e){Object.defineProperty(o,e,Object.getOwnPropertyDescriptor(r,e))})}return o}function g(e){return(g="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var O,w,j="function"==typeof fetch?fetch:void 0;if(void 0!==q&&q.fetch?j=q.fetch:"undefined"!=typeof window&&window.fetch&&(j=window.fetch),(0,b.hasXMLHttpRequest)()&&(void 0!==q&&q.XMLHttpRequest?O=q.XMLHttpRequest:"undefined"!=typeof window&&window.XMLHttpRequest&&(O=window.XMLHttpRequest)),"function"==typeof ActiveXObject&&(void 0!==q&&q.ActiveXObject?w=q.ActiveXObject:"undefined"!=typeof window&&window.ActiveXObject&&(w=window.ActiveXObject)),!(j="function"!=typeof j?void 0:j)&&!O&&!w)try{j=e("cross-fetch")}catch(e){}var P=["__proto__","constructor","prototype"],S=function(e,t){if(t&&"object"===g(t)){for(var n="",o=0,r=Object.keys(t);o<r.length;o++){var i=r[o];-1<P.indexOf(i)||(n+="&"+encodeURIComponent(i)+"="+encodeURIComponent(t[i]))}if(!n)return e;e=e+(-1!==e.indexOf("?")?"&":"?")+n.slice(1)}return e},x=function(e,t,n,o){function r(t){if(!t.ok)return n(t.statusText||"Error",{status:t.status});t.text().then(function(e){n(null,{status:t.status,data:e})}).catch(n)}if(o){o=o(e,t);if(o instanceof Promise)return void o.then(r).catch(n)}("function"==typeof fetch?fetch:j)(e,t).then(r).catch(n)};o.default=function(e,t,n,o){if("function"==typeof n&&(o=n,n=void 0),o=o||function(){},j&&0!==t.indexOf("file:")){var r=e,i=t,a=n,s=o,u=(r.queryStringParams&&(i=S(i,r.queryStringParams)),m({},"function"==typeof r.customHeaders?r.customHeaders():r.customHeaders)),c=("undefined"==typeof window&&void 0!==q&&void 0!==q.process&&q.process.versions&&q.process.versions.node&&(u["User-Agent"]="i18next-http-backend (node/".concat(q.process.version,"; ").concat(q.process.platform," ").concat(q.process.arch,")")),a&&(u["Content-Type"]="application/json"),"function"==typeof r.requestOptions?r.requestOptions(a):r.requestOptions),f=m({method:a?"POST":"GET",body:a?r.stringify(a):void 0,headers:u},r._omitFetchOptions?{}:c),a="function"==typeof r.alternateFetch&&1<=r.alternateFetch.length?r.alternateFetch:void 0;try{x(i,f,s,a)}catch(e){if(!c||0===Object.keys(c).length||!e.message||e.message.indexOf("not implemented")<0)return s(e);try{Object.keys(c).forEach(function(e){delete f[e]}),x(i,f,s,a),r._omitFetchOptions=!0}catch(e){s(e)}}}else if((0,b.hasXMLHttpRequest)()||"function"==typeof ActiveXObject){var u=e,c=t,i=n,l=o;i&&"object"===g(i)&&(i=S("",i).slice(1)),u.queryStringParams&&(c=S(c,u.queryStringParams));try{var d=O?new O:new w("MSXML2.XMLHTTP.3.0"),p=(d.open(i?"POST":"GET",c,1),u.crossDomain||d.setRequestHeader("X-Requested-With","XMLHttpRequest"),d.withCredentials=!!u.withCredentials,i&&d.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),d.overrideMimeType&&d.overrideMimeType("application/json"),u.customHeaders);if(p="function"==typeof p?p():p)for(var y=0,h=Object.keys(p);y<h.length;y++){var v=h[y];-1<P.indexOf(v)||d.setRequestHeader(v,p[v])}d.onreadystatechange=function(){3<d.readyState&&l(400<=d.status?d.statusText:null,{status:d.status,data:d.responseText})},d.send(i)}catch(e){console&&console.log(e)}}else o(new Error("No fetch and no xhr implementation found!"))};n.exports=o.default}.call(this)}.call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./utils.js":3,"cross-fetch":4}],3:[function(e,t,n){function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);n<t;n++)o[n]=e[n];return o}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}Object.defineProperty(n,"__esModule",{value:!0}),n.defaults=function(r){return i.call(a.call(arguments,1),function(e){if(e)for(var t=0,n=Object.keys(e);t<n.length;t++){var o=n[t];-1<u.indexOf(o)||void 0===r[o]&&(r[o]=e[o])}}),r},n.hasXMLHttpRequest=function(){return"function"==typeof XMLHttpRequest||"object"===("undefined"==typeof XMLHttpRequest?"undefined":o(XMLHttpRequest))},n.interpolate=function(e,n){return e.replace(f,function(e,t){var t=t.trim();return!(-1<u.indexOf(t))&&null!=(t=n[t])?t:e})},n.interpolateUrl=function(e,r){var i=!1,e=e.replace(f,function(e,t){t=t.trim();if(-1<u.indexOf(t))return e;t=r[t];if(null==t)return e;var n,t=String(t).split("+"),o=((e,t)=>{var n,o,r,i,a="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(a)return r=!(o=!0),{s:function(){a=a.call(e)},n:function(){var e=a.next();return o=e.done,e},e:function(e){r=!0,n=e},f:function(){try{o||null==a.return||a.return()}finally{if(r)throw n}}};if(Array.isArray(e)||(a=((e,t)=>{var n;if(e)return"string"==typeof e?s(e,t):"Map"===(n="Object"===(n={}.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:n)||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0})(e))||t&&e&&"number"==typeof e.length)return a&&(e=a),i=0,{s:t=function(){},n:function(){return i>=e.length?{done:!0}:{done:!1,value:e[i++]}},e:function(e){throw e},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")})(t);try{for(o.s();!(n=o.n()).done;)if(!c(n.value))return i=!0,e}catch(e){o.e(e)}finally{o.f()}return t.join("+")});return i?null:e},n.isSafeUrlSegment=c,n.makePromise=function(e){if((e=>e&&"function"==typeof e.then)(e))return e;return Promise.resolve(e)},n.redactUrlCredentials=function(t){if("string"!=typeof t||0===t.length)return t;try{var e=new URL(t);return e.username||e.password?(e.username="",e.password="",e.toString()):t}catch(e){return t.replace(/(\/\/)[^/@\s]+@/g,"$1")}},n.sanitizeLogValue=function(e){return"string"!=typeof e?e:e.replace(/[\r\n\x00-\x1F\x7F]/g," ")};var n=[],i=n.forEach,a=n.slice,u=["__proto__","constructor","prototype"];function c(e){return"string"==typeof e&&!(0===e.length||128<e.length||-1<u.indexOf(e)||-1<e.indexOf("..")||-1<e.indexOf("/")||-1<e.indexOf("\\")||/[?#%\s@]/.test(e)||/[\x00-\x1F\x7F]/.test(e))}var f=/\{\{(.+?)\}\}/g},{}],4:[function(e,t,n){},{}]},{},[1])(1)}); \ No newline at end of file
lib/index.js+22 −7 modified@@ -1,4 +1,4 @@ -import { makePromise, interpolate } from './utils.js' +import { makePromise, interpolateUrl, sanitizeLogValue, redactUrlCredentials } from './utils.js' import request from './request.js' const getDefaults = () => { @@ -61,7 +61,12 @@ class Backend { loadPath.then(resolvedLoadPath => { if (!resolvedLoadPath) return callback(null, {}) - const url = interpolate(resolvedLoadPath, { lng: languages.join('+'), ns: namespaces.join('+') }) + const url = interpolateUrl(resolvedLoadPath, { lng: languages.join('+'), ns: namespaces.join('+') }) + if (url == null) { + const safeLngs = languages.map(sanitizeLogValue).join(', ') + const safeNss = namespaces.map(sanitizeLogValue).join(', ') + return callback(new Error('i18next-http-backend: unsafe lng/ns value — refusing to build request URL for languages=[' + safeLngs + '] namespaces=[' + safeNss + ']'), false) + } this.loadUrl(url, callback, loadUrlLanguages, loadUrlNamespaces) }) } @@ -71,9 +76,14 @@ class Backend { const ns = (typeof namespaces === 'string') ? [namespaces] : namespaces // parseLoadPayload — default undefined const payload = this.options.parseLoadPayload(lng, ns) + // URLs in error callbacks are redacted (user:password credentials + // stripped) and log-sanitised (CR/LF/control chars replaced) to prevent + // leaking credentials that may have been embedded in loadPath and to + // prevent log forging via crafted `lng`/`ns` values (CWE-117). + const safeUrl = sanitizeLogValue(redactUrlCredentials(url)) this.options.request(this.options, url, payload, (err, res) => { - if (res && ((res.status >= 500 && res.status < 600) || !res.status)) return callback('failed loading ' + url + '; status code: ' + res.status, true /* retry */) - if (res && res.status >= 400 && res.status < 500) return callback('failed loading ' + url + '; status code: ' + res.status, false /* no retry */) + if (res && ((res.status >= 500 && res.status < 600) || !res.status)) return callback('failed loading ' + safeUrl + '; status code: ' + res.status, true /* retry */) + if (res && res.status >= 400 && res.status < 500) return callback('failed loading ' + safeUrl + '; status code: ' + res.status, false /* no retry */) if (!res && err && err.message) { const errorMessage = err.message.toLowerCase() // for example: @@ -87,7 +97,7 @@ class Backend { 'load' ].find((term) => errorMessage.indexOf(term) > -1) if (isNetworkError) { - return callback('failed loading ' + url + ': ' + err.message, true /* retry */) + return callback('failed loading ' + safeUrl + ': ' + sanitizeLogValue(err.message), true /* retry */) } } if (err) return callback(err, false) @@ -100,7 +110,7 @@ class Backend { ret = res.data } } catch (e) { - parseErr = 'failed parsing ' + url + ' to json' + parseErr = 'failed parsing ' + safeUrl + ' to json' } if (parseErr) return callback(parseErr, false) callback(null, ret) @@ -120,7 +130,12 @@ class Backend { if (typeof this.options.addPath === 'function') { addPath = this.options.addPath(lng, namespace) } - const url = interpolate(addPath, { lng, ns: namespace }) + const url = interpolateUrl(addPath, { lng, ns: namespace }) + if (url == null) { + finished += 1 + if (callback && finished === languages.length) callback(dataArray, resArray) + return + } this.options.request(this.options, url, payload, (data, res) => { // TODO: if res.status === 4xx do log
lib/request.js+16 −7 modified@@ -35,11 +35,16 @@ if (!fetchApi && !XmlHttpRequestApi && !ActiveXObjectApi) { } catch (e) {} } +const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'] + const addQueryString = (url, params) => { if (params && typeof params === 'object') { let queryString = '' - // Must encode data - for (const paramName in params) { + // Must encode data. Iterate own enumerable keys only (Object.keys) and + // drop prototype-chain names so a polluted Object.prototype cannot leak + // into the URL. + for (const paramName of Object.keys(params)) { + if (UNSAFE_KEYS.indexOf(paramName) > -1) continue queryString += '&' + encodeURIComponent(paramName) + '=' + encodeURIComponent(params[paramName]) } if (!queryString) return url @@ -71,8 +76,6 @@ const fetchIt = (url, fetchOptions, callback, altFetch) => { } } -let omitFetchOptions = false - // fetch api stuff const requestWithFetch = (options, url, payload, callback) => { if (options.queryStringParams) { @@ -86,11 +89,15 @@ const requestWithFetch = (options, url, payload, callback) => { } if (payload) headers['Content-Type'] = 'application/json' const reqOptions = typeof options.requestOptions === 'function' ? options.requestOptions(payload) : options.requestOptions + // `omitFetchOptions` is stored on the backend `options` object (per-instance) + // instead of on a module-level flag. This prevents cross-instance + // pollution: one instance hitting a "not implemented" fetch no longer + // silently strips requestOptions from every other instance in the process. const fetchOptions = { method: payload ? 'POST' : 'GET', body: payload ? options.stringify(payload) : undefined, headers, - ...(omitFetchOptions ? {} : reqOptions) + ...(options._omitFetchOptions ? {} : reqOptions) } const altFetch = typeof options.alternateFetch === 'function' && options.alternateFetch.length >= 1 ? options.alternateFetch : undefined try { @@ -104,7 +111,8 @@ const requestWithFetch = (options, url, payload, callback) => { delete fetchOptions[opt] }) fetchIt(url, fetchOptions, callback, altFetch) - omitFetchOptions = true + // Per-instance flag: only affects this backend, not siblings. + options._omitFetchOptions = true } catch (err) { callback(err) } @@ -139,7 +147,8 @@ const requestWithXmlHttpRequest = (options, url, payload, callback) => { let h = options.customHeaders h = typeof h === 'function' ? h() : h if (h) { - for (const i in h) { + for (const i of Object.keys(h)) { + if (UNSAFE_KEYS.indexOf(i) > -1) continue x.setRequestHeader(i, h[i]) } }
lib/utils.js+79 −2 modified@@ -2,17 +2,69 @@ const arr = [] const each = arr.forEach const slice = arr.slice +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] } } }) return obj } +// Returns true if `v` can be safely interpolated into a URL path segment. +// 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, URL structure characters, control characters, prototype keys, +// and oversized inputs. `+` is allowed — callers use `languages.join('+')` +// to request multiple languages in one request. +export function isSafeUrlSegment (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 + // Block characters that would terminate/restructure the URL: + // `?` (starts query), `#` (starts fragment), `%` (percent-encoded bypass + // of `..`/`/` via `%2E%2E`/`%2F`), space (ambiguous), `@` (authority + // boundary in userinfo-containing URLs). + if (/[?#%\s@]/.test(v)) return false + // eslint-disable-next-line no-control-regex + if (/[\x00-\x1F\x7F]/.test(v)) return false + return true +} + +// Strip control characters (CR/LF/NUL/C0/C1) from a string so it cannot +// inject fake log lines when concatenated into an error message (CWE-117). +export function sanitizeLogValue (v) { + if (typeof v !== 'string') return v + // eslint-disable-next-line no-control-regex + return v.replace(/[\r\n\x00-\x1F\x7F]/g, ' ') +} + +// Redact user:password credentials from a URL-like string before logging. +// Handles both full URLs and malformed ones — on parse failure, falls back +// to a regex that matches the userinfo portion of the authority. +export function redactUrlCredentials (u) { + if (typeof u !== 'string' || u.length === 0) return u + try { + const parsed = new URL(u) + if (parsed.username || parsed.password) { + parsed.username = '' + parsed.password = '' + return parsed.toString() + } + return u + } catch (e) { + return u.replace(/(\/\/)[^/@\s]+@/g, '$1') + } +} + export function hasXMLHttpRequest () { return (typeof XMLHttpRequest === 'function' || typeof XMLHttpRequest === 'object') } @@ -46,7 +98,32 @@ export function makePromise (maybePromise) { const interpolationRegexp = /\{\{(.+?)\}\}/g export function interpolate (str, data) { 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 }) } + +// URL-specific variant: reject values that fail the URL-segment safety +// check. Returns `null` if any substitution is unsafe — callers should bail +// out cleanly rather than issue the request. For multi-value joins +// (`en+de`), validates each `+`-separated segment independently. +export function interpolateUrl (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 (!isSafeUrlSegment(seg)) { + unsafe = true + return match + } + } + return segments.join('+') + }) + return unsafe ? null : result +}
package.json+7 −7 modified@@ -31,9 +31,9 @@ }, "types": "./index.d.mts", "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", "browserify": "17.0.1", "dtslint": "4.2.1", @@ -45,14 +45,14 @@ "eslint-plugin-require-path-exists": "1.1.9", "eslint-plugin-standard": "5.0.0", "expect.js": "0.3.1", - "i18next": "24.0.0", + "i18next": "26.0.6", "json-server": "0.17.4", "json5": "2.2.3", "jsonc-parser": "3.3.1", - "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", "xmlhttprequest": "1.8.0" },
test/security.spec.js+118 −0 added@@ -0,0 +1,118 @@ +import expect from 'expect.js' +import { + interpolate, + interpolateUrl, + isSafeUrlSegment, + sanitizeLogValue, + redactUrlCredentials +} from '../lib/utils.js' + +// Security tests for fixes shipped in 3.0.5. +// See CHANGELOG for associated GHSA advisory. + +describe('security', () => { + describe('isSafeUrlSegment', () => { + it('accepts arbitrary language-code shapes (i18next permits any shape)', () => { + expect(isSafeUrlSegment('en')).to.be(true) + expect(isSafeUrlSegment('de-DE')).to.be(true) + expect(isSafeUrlSegment('en_US')).to.be(true) + expect(isSafeUrlSegment('zh-Hant-HK')).to.be(true) + expect(isSafeUrlSegment('pirate-speak')).to.be(true) + expect(isSafeUrlSegment('my-custom.ns')).to.be(true) + }) + + it('rejects path-traversal / URL-structure / control-char payloads', () => { + expect(isSafeUrlSegment('../etc/passwd')).to.be(false) + expect(isSafeUrlSegment('..')).to.be(false) + expect(isSafeUrlSegment('foo/bar')).to.be(false) + expect(isSafeUrlSegment('foo\\bar')).to.be(false) + expect(isSafeUrlSegment('en?admin=true')).to.be(false) + expect(isSafeUrlSegment('en#frag')).to.be(false) + expect(isSafeUrlSegment('en%2F..')).to.be(false) + expect(isSafeUrlSegment('en user')).to.be(false) + expect(isSafeUrlSegment('en@evil.com')).to.be(false) + expect(isSafeUrlSegment('__proto__')).to.be(false) + expect(isSafeUrlSegment('en\r\nX-Injected: bad')).to.be(false) + expect(isSafeUrlSegment('')).to.be(false) + expect(isSafeUrlSegment('a'.repeat(200))).to.be(false) + }) + }) + + describe('interpolate', () => { + it('does not pollute Object.prototype via __proto__ key in data', () => { + // interpolate is not a mutation API (it just reads data[key]) but we + // still guard the key name to prevent surprising behaviour from a + // polluted template. + const out = interpolate('x {{__proto__}} y', { __proto__: { polluted: true } }) + // The placeholder is returned unchanged (no substitution for unsafe keys) + expect(out).to.equal('x {{__proto__}} y') + expect(({}).polluted).to.be(undefined) + }) + + it('still substitutes safe keys', () => { + expect(interpolate('hi {{name}}', { name: 'world' })).to.equal('hi world') + }) + }) + + describe('interpolateUrl', () => { + it('accepts plain codes and multi-lang + joins', () => { + expect(interpolateUrl('/locales/{{lng}}/{{ns}}.json', { lng: 'en', ns: 'common' })) + .to.equal('/locales/en/common.json') + expect(interpolateUrl('/locales/{{lng}}/{{ns}}.json', { lng: 'en+de', ns: 'a+b' })) + .to.equal('/locales/en+de/a+b.json') + }) + + it('returns null for path-traversal values', () => { + expect(interpolateUrl('/locales/{{lng}}/{{ns}}.json', { lng: '../etc/passwd', ns: 'x' })) + .to.equal(null) + expect(interpolateUrl('/locales/{{lng}}/{{ns}}.json', { lng: 'en', ns: '..' })) + .to.equal(null) + }) + + it('returns null for URL-structure injection (query-string, fragment)', () => { + expect(interpolateUrl('/locales/{{lng}}/{{ns}}.json', { lng: 'en?admin=true', ns: 'x' })) + .to.equal(null) + expect(interpolateUrl('/locales/{{lng}}/{{ns}}.json', { lng: 'en#frag', ns: 'x' })) + .to.equal(null) + }) + + it('returns null when any segment of a multi-lang + join is unsafe', () => { + expect(interpolateUrl('/locales/{{lng}}.json', { lng: 'en+../etc/passwd' })) + .to.equal(null) + }) + + it('ignores __proto__ placeholder', () => { + expect(interpolateUrl('/{{__proto__}}/x', { __proto__: { a: 1 } })) + .to.equal('/{{__proto__}}/x') + }) + }) + + describe('sanitizeLogValue', () => { + it('strips CR, LF, NUL and other control chars', () => { + expect(sanitizeLogValue('en\r\n2026-04-18 admin login')) + .to.equal('en 2026-04-18 admin login') + expect(sanitizeLogValue('en\u0000')).to.equal('en ') + }) + it('passes non-strings through unchanged', () => { + expect(sanitizeLogValue(undefined)).to.equal(undefined) + expect(sanitizeLogValue(42)).to.equal(42) + }) + }) + + describe('redactUrlCredentials', () => { + it('strips user:password from URLs', () => { + expect(redactUrlCredentials('https://user:pass@example.com/locales/en.json')) + .to.equal('https://example.com/locales/en.json') + expect(redactUrlCredentials('https://example.com/locales/en.json')) + .to.equal('https://example.com/locales/en.json') + }) + it('handles malformed URLs (regex fallback)', () => { + const out = redactUrlCredentials('//user:pass@example.com/path') + expect(out.indexOf('user:pass@')).to.equal(-1) + }) + it('passes non-strings / empty through', () => { + expect(redactUrlCredentials(null)).to.equal(null) + expect(redactUrlCredentials('')).to.equal('') + }) + }) +})
tsconfig.json+1 −0 modified@@ -2,6 +2,7 @@ "compilerOptions": { "module": "commonjs", "target": "es5", + "ignoreDeprecations": "6.0", "lib": ["es6", "dom"], "jsx": "react", "moduleResolution": "node",
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
4News mentions
0No linked articles in our index yet.