i18next-http-backend
Version:
i18next-http-backend is a backend layer for i18next using in Node.js, in the browser and for Deno.
163 lines (147 loc) • 5.67 kB
JavaScript
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 of Object.keys(source)) {
if (UNSAFE_KEYS.indexOf(prop) > -1) continue
if (obj[prop] === undefined) obj[prop] = source[prop]
}
}
})
return obj
}
// Shared denylist: the patterns that are always unsafe to interpolate into a
// URL, regardless of whether the value is a language code or a namespace
// name. Blocks directory escape (`..`), Windows path separators (`\`),
// URL-structure characters that would terminate the path (`?`, `#`, `%`,
// whitespace, `@`), control characters, prototype keys, empty strings, and
// oversized inputs. The one dimension that differs per-key is whether `/`
// is allowed — see the two per-key helpers below.
function isSafeUrlSegmentBase (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) 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
}
// Strict — also rejects `/`. Applied to `lng`:
// no legitimate BCP-47 / i18next language-code shape
// (https://www.i18next.com/how-to/faq#how-should-the-language-codes-be-formatted)
// contains `/`, so allowing it would only enable attacks.
export function isSafeLangUrlSegment (v) {
if (!isSafeUrlSegmentBase(v)) return false
if (v.indexOf('/') > -1) return false
return true
}
// Loose — allows `/`. Applied to `ns`:
// nested namespace names like `a/b` that map to subfolder URL layouts
// (`/locales/en/a/b.json`) are a documented i18next pattern and were
// rejected in 3.0.5 as an unintended regression. Directory escape is still
// prevented — `..` is still blocked, `\` is still blocked, URL-structure
// characters are still blocked — so the security fix remains in force for
// every concrete attack pattern from the original advisory.
export function isSafeNsUrlSegment (v) {
return isSafeUrlSegmentBase(v)
}
// Backwards-compatible alias for the strict check. Kept because 3.0.5
// exported this symbol; external callers (if any) get the pre-fix behaviour.
export const isSafeUrlSegment = isSafeLangUrlSegment
// Per-interpolation-key routing: `lng` gets strict, `ns` gets loose.
// Unknown keys fall back to strict (fail-closed).
const SAFETY_CHECK_BY_KEY = {
lng: isSafeLangUrlSegment,
ns: isSafeNsUrlSegment
}
// 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')
}
/**
* Determine whether the given `maybePromise` is a Promise.
*
* @param {*} maybePromise
*
* @returns {Boolean}
*/
function isPromise (maybePromise) {
return !!maybePromise && typeof maybePromise.then === 'function'
}
/**
* Convert any value to a Promise than will resolve to this value.
*
* @param {*} maybePromise
*
* @returns {Promise}
*/
export function makePromise (maybePromise) {
if (isPromise(maybePromise)) {
return maybePromise
}
return Promise.resolve(maybePromise)
}
const interpolationRegexp = /\{\{(.+?)\}\}/g
export function interpolate (str, data) {
return str.replace(interpolationRegexp, (match, key) => {
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 check = SAFETY_CHECK_BY_KEY[k] || isSafeLangUrlSegment // fail-closed on unknown keys
const segments = String(value).split('+')
for (const seg of segments) {
if (!check(seg)) {
unsafe = true
return match
}
}
return segments.join('+')
})
return unsafe ? null : result
}