i18next-fs-backend
Version:
i18next-fs-backend is a backend layer for i18next using in Node.js and for Deno to load translations from the filesystem.
157 lines (139 loc) • 5.31 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
// filesystem path, regardless of whether the value is a language code or a
// namespace name. Blocks directory escape (`..`), Windows path separators
// (`\`), control characters, prototype keys, empty strings, and oversized
// inputs. Anything that gets past this is safe *except* for whether it is
// allowed to contain a forward slash — see the two per-key helpers below.
function isSafeSegmentBase (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
// eslint-disable-next-line no-control-regex
if (/[\x00-\x1F\x7F]/.test(v)) return false
return true
}
// Strict — also rejects forward slash. 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 isSafeLangSegment (v) {
if (!isSafeSegmentBase(v)) return false
if (v.indexOf('/') > -1) return false
return true
}
// Loose — allows forward slash. Applied to `ns`:
// nested namespace names like `a/b` that map to subfolder locale files
// (`public/locales/en/a/b.json`) are a documented i18next pattern and were
// rejected in 2.6.4 as an unintended regression (see issue #74). Directory
// escape is still prevented — `..` is still blocked, `\` is still blocked,
// and the 2.6.4 security fix remains in force for every concrete attack
// pattern from the original advisory.
export function isSafeNsSegment (v) {
return isSafeSegmentBase(v)
}
// Backwards-compatible alias for the strict check. Kept because 2.6.4
// exported this symbol; external callers (if any) get the pre-fix behaviour.
export const isSafePathSegment = isSafeLangSegment
// Per-interpolation-key routing: `lng` gets strict, `ns` gets loose.
// Unknown keys fall back to strict (fail-closed).
const SAFETY_CHECK_BY_KEY = {
lng: isSafeLangSegment,
ns: isSafeNsSegment
}
export function debounce (func, wait, immediate) {
let timeout
return function () {
const context = this
const args = arguments
const later = function () {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
function getLastOfPath (object, path, Empty) {
function cleanKey (key) {
return (key && key.indexOf('###') > -1) ? key.replace(/###/g, '.') : key
}
const stack = (typeof path !== 'string') ? [].concat(path) : path.split('.')
while (stack.length > 1) {
if (!object) return {}
const key = cleanKey(stack.shift())
if (!object[key] && Empty) object[key] = new Empty()
object = object[key]
}
if (!object) return {}
return {
obj: object,
k: cleanKey(stack.shift())
}
}
export function setPath (object, path, newValue) {
const { obj, k } = getLastOfPath(object, path, Object)
if (Array.isArray(obj) && isNaN(k)) throw new Error(`Cannot create property "${k}" here since object is an array`)
obj[k] = newValue
}
export function pushPath (object, path, newValue, concat) {
const { obj, k } = getLastOfPath(object, path, Object)
obj[k] = obj[k] || []
if (concat) obj[k] = obj[k].concat(newValue)
if (!concat) obj[k].push(newValue)
}
export function getPath (object, path) {
const { obj, k } = getLastOfPath(object, path)
if (!obj) return undefined
return obj[k]
}
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
})
}
// 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 check = SAFETY_CHECK_BY_KEY[k] || isSafeLangSegment // 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
}