Skip to content

Commit 8c82564

Browse files
committed
security: hardening for 26.0.6 — nesting-options warning, regexEscape unescape delimiters, log-inject stripping
1 parent 0cb018c commit 8c82564

6 files changed

Lines changed: 51 additions & 6 deletions

File tree

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,11 @@ dist/**/*
2424

2525
# vitest temp / cache files
2626
tsconfig.vitest-temp.json
27-
tsconfig.nonEsModuleInterop.vitest-temp.json
27+
tsconfig.nonEsModuleInterop.vitest-temp.json
28+
29+
# Secrets & credentials
30+
.env
31+
.env.*
32+
!.env.example
33+
*.pem
34+
*.key

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 26.0.6
2+
3+
Security release — all issues found via an internal audit. GHSA advisory filed after release.
4+
5+
- security: warn when a translation string combines `escapeValue: false` with interpolated variables inside a `$t(key, { ... "{{var}}" ... })` nesting-options block. In that narrow combination, attacker-controlled string values containing `"` can break out of the JSON options literal and inject additional nesting options (e.g. redirect `lng`/`ns`). The default `escapeValue: true` configuration is unaffected because HTML-escaping neutralises the quote before `JSON.parse`. See the security docs for mitigation guidance (GHSA-TBD)
6+
- security: apply `regexEscape` to `unescapePrefix` / `unescapeSuffix` on par with the other interpolation delimiters. Prevents ReDoS (catastrophic-backtracking) when a misconfigured delimiter contains regex metacharacters, and fixes silent breakage of the `{{- var}}` syntax when the delimiter contains characters like `(`, `[`, `.`
7+
- security: strip CR/LF/NUL and other C0/C1 control characters from string log arguments to prevent log forging via user-controlled translation keys, language codes, namespaces, or interpolation variable names (CWE-117)
8+
- chore: ignore `.env*` and `*.pem`/`*.key` files in `.gitignore`
9+
110
## 26.0.5
211

312
- fix: `cloneInstance().changeLanguage()` no longer fails to update language state when the target language is not yet loaded — a race between `init()`'s deferred `load()` and the user's `changeLanguage()` could overwrite `isLanguageChangingTo`, causing `setLngProps` to be skipped [2422](https://github.com/i18next/i18next/issues/2422)

i18next.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
}
236236
forward(args, lvl, prefix, debugOnly) {
237237
if (debugOnly && !this.debug) return null;
238+
args = args.map(a => isString(a) ? a.replace(/[\r\n\x00-\x1F\x7F]/g, ' ') : a);
238239
if (isString(args[0])) args[0] = `${prefix}${this.prefix} ${args[0]}`;
239240
return this.logger[lvl](args);
240241
}
@@ -1140,8 +1141,8 @@
11401141
this.prefix = prefix ? regexEscape(prefix) : prefixEscaped || '{{';
11411142
this.suffix = suffix ? regexEscape(suffix) : suffixEscaped || '}}';
11421143
this.formatSeparator = formatSeparator || ',';
1143-
this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix || '-';
1144-
this.unescapeSuffix = this.unescapePrefix ? '' : unescapeSuffix || '';
1144+
this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix ? regexEscape(unescapePrefix) : '-';
1145+
this.unescapeSuffix = this.unescapePrefix ? '' : unescapeSuffix ? regexEscape(unescapeSuffix) : '';
11451146
this.nestingPrefix = nestingPrefix ? regexEscape(nestingPrefix) : nestingPrefixEscaped || regexEscape('$t(');
11461147
this.nestingSuffix = nestingSuffix ? regexEscape(nestingSuffix) : nestingSuffixEscaped || regexEscape(')');
11471148
this.nestingOptionsSeparator = nestingOptionsSeparator || ',';
@@ -1188,6 +1189,9 @@
11881189
});
11891190
};
11901191
this.resetRegExp();
1192+
if (!this.escapeValue && typeof str === 'string' && /\$t\([^)]*\{[^}]*\{\{/.test(str)) {
1193+
this.logger.warn('nesting options string contains interpolated variables with escapeValue: false — ' + 'if any of those values are attacker-controlled they can inject additional ' + 'nesting options (e.g. redirect lng/ns). Sanitise untrusted input before passing ' + 'it to t(), or keep escapeValue: true.');
1194+
}
11911195
const missingInterpolationHandler = options?.missingInterpolationHandler || this.options.missingInterpolationHandler;
11921196
const skipOnVariables = options?.interpolation?.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables;
11931197
const todos = [{

i18next.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Interpolator.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ class Interpolator {
6666

6767
this.formatSeparator = formatSeparator || ',';
6868

69-
this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix || '-';
70-
this.unescapeSuffix = this.unescapePrefix ? '' : unescapeSuffix || '';
69+
this.unescapePrefix = unescapeSuffix ? '' : unescapePrefix ? regexEscape(unescapePrefix) : '-';
70+
this.unescapeSuffix = this.unescapePrefix
71+
? ''
72+
: unescapeSuffix
73+
? regexEscape(unescapeSuffix)
74+
: '';
7175

7276
this.nestingPrefix = nestingPrefix
7377
? regexEscape(nestingPrefix)
@@ -157,6 +161,22 @@ class Interpolator {
157161

158162
this.resetRegExp();
159163

164+
// Security warning: when escapeValue is false AND the translation embeds
165+
// interpolation placeholders inside a $t() nesting options block (i.e.
166+
// $t(key, { ... "{{var}}" ... })), attacker-controlled string values that
167+
// contain `"` can break out of the JSON string literal and inject extra
168+
// nesting options (e.g. redirect lng/ns). This is safe under the default
169+
// escapeValue: true, where HTML-escaping neutralises the quote before
170+
// JSON.parse. See CHANGELOG entry for 26.0.6 and the security docs.
171+
if (!this.escapeValue && typeof str === 'string' && /\$t\([^)]*\{[^}]*\{\{/.test(str)) {
172+
this.logger.warn(
173+
'nesting options string contains interpolated variables with escapeValue: false — ' +
174+
'if any of those values are attacker-controlled they can inject additional ' +
175+
'nesting options (e.g. redirect lng/ns). Sanitise untrusted input before passing ' +
176+
'it to t(), or keep escapeValue: true.',
177+
);
178+
}
179+
160180
const missingInterpolationHandler =
161181
options?.missingInterpolationHandler || this.options.missingInterpolationHandler;
162182

src/logger.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class Logger {
5151

5252
forward(args, lvl, prefix, debugOnly) {
5353
if (debugOnly && !this.debug) return null;
54+
// Strip control characters from string args to prevent log forging via
55+
// user-controlled translation keys, languages, namespaces, or interpolation
56+
// variable names (CWE-117).
57+
// eslint-disable-next-line no-control-regex
58+
args = args.map((a) => (isString(a) ? a.replace(/[\r\n\x00-\x1F\x7F]/g, ' ') : a));
5459
if (isString(args[0])) args[0] = `${prefix}${this.prefix} ${args[0]}`;
5560
return this.logger[lvl](args);
5661
}

0 commit comments

Comments
 (0)