perf(obfuscation): cache per-word regexes instead of recompiling every request (#4109)

Integrated into release/v3.8.28 (r8)
This commit is contained in:
NOXX - Commiter
2026-06-17 23:25:58 +03:00
committed by GitHub
parent 64be300566
commit 8330111c93
3 changed files with 47 additions and 4 deletions

View File

@@ -38,12 +38,26 @@ function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Per-word regex cache — avoids recompiling one RegExp per word on every request body.
// Bounded by distinct configured words; global regexes are safe to reuse (String.replace
// resets lastIndex).
const _obfuscationRegexCache = new Map<string, RegExp>();
function getObfuscationRegex(word: string): RegExp {
let regex = _obfuscationRegexCache.get(word);
if (!regex) {
if (_obfuscationRegexCache.size > 2000) _obfuscationRegexCache.clear();
regex = new RegExp(escapeRegex(word), "gi");
_obfuscationRegexCache.set(word, regex);
}
return regex;
}
export function obfuscateSensitiveWords(text: string): string {
if (!text || words.length === 0) return text;
let result = text;
for (const word of words) {
if (!word) continue;
const regex = new RegExp(escapeRegex(word), "gi");
const regex = getObfuscationRegex(word);
result = result.replace(regex, (m) => (m.length <= 1 ? m : m[0] + ZWJ + m.slice(1)));
}
return result;

View File

@@ -40,14 +40,28 @@ function obfuscateWord(word: string): string {
return word[0] + ZWJ + word.slice(1);
}
// Per-word regex cache — obfuscateSensitiveWords recompiles one RegExp per word on every
// request body otherwise. Bounded by distinct configured words; global regexes are safe to
// reuse because String.replace resets lastIndex.
const _obfuscationRegexCache = new Map<string, RegExp>();
function getObfuscationRegex(word: string): RegExp {
let regex = _obfuscationRegexCache.get(word);
if (!regex) {
if (_obfuscationRegexCache.size > 2000) _obfuscationRegexCache.clear();
regex = new RegExp(escapeRegex(word), "gi");
_obfuscationRegexCache.set(word, regex);
}
return regex;
}
export function obfuscateSensitiveWords(text: string): string {
if (!text || sensitiveWords.length === 0) return text;
let result = text;
for (const word of sensitiveWords) {
if (!word) continue;
// Case-insensitive replacement
const regex = new RegExp(escapeRegex(word), "gi");
// Case-insensitive replacement (cached: see getObfuscationRegex)
const regex = getObfuscationRegex(word);
result = result.replace(regex, (match) => obfuscateWord(match));
}
return result;

View File

@@ -269,12 +269,27 @@ function obfuscateWord(word: string): string {
* list instead of the module-level singleton, so concurrent requests with
* different op configs do not race.
*/
// Per-word regex cache: obfuscateWithList runs over the whole request body on every
// request when obfuscation is enabled, recompiling one RegExp per word each time. The
// word list is stable per op config, so memoize. Bounded by distinct configured words
// (with a defensive cap). Global regexes are safe to reuse: String.replace resets lastIndex.
const _obfuscationRegexCache = new Map<string, RegExp>();
function getObfuscationRegex(word: string): RegExp {
let regex = _obfuscationRegexCache.get(word);
if (!regex) {
if (_obfuscationRegexCache.size > 2000) _obfuscationRegexCache.clear();
regex = new RegExp(escapeRegex(word), "gi");
_obfuscationRegexCache.set(word, regex);
}
return regex;
}
function obfuscateWithList(text: string, words: string[]): string {
if (!text || words.length === 0) return text;
let result = text;
for (const word of words) {
if (!word) continue;
const regex = new RegExp(escapeRegex(word), "gi");
const regex = getObfuscationRegex(word);
result = result.replace(regex, (match) => obfuscateWord(match));
}
return result;