fix(logger): mask secure values surrounded by non-word characters (#22553)

`\b` only asserts a boundary next to a word character, so text rules like
`P@ssw0rd!` never matched anything. Now it is only added to the sides where
the value itself begins or ends with a word character.
This commit is contained in:
Mohamed Mostafa
2026-07-26 20:34:47 +03:00
committed by GitHub
parent cdd448800e
commit 8ac6bfcc3b
2 changed files with 41 additions and 2 deletions
@@ -34,7 +34,7 @@ export class SecureValuesPreprocessor {
if (rule.length === 0) {
throw new Error(`${JSON.stringify(rule)} -> The value must not be empty`);
}
pattern = `\\b${escapeRegExp(rule)}\\b`;
pattern = toExactMatchPattern(rule);
} else if (isPlainObject(rule)) {
if (isLogFilterRegex(rule)) {
if (typeof rule.pattern !== 'string' || rule.pattern.length === 0) {
@@ -45,7 +45,7 @@ export class SecureValuesPreprocessor {
if (typeof rule.text !== 'string' || rule.text.length === 0) {
throw new Error(`${JSON.stringify(rule)} -> The value of 'text' must be a valid non-empty string`);
}
pattern = `\\b${escapeRegExp(rule.text)}\\b`;
pattern = toExactMatchPattern(rule.text);
}
if (!pattern) {
throw new Error(`${JSON.stringify(rule)} -> Must either have a field named 'pattern' or 'text'`);
@@ -136,3 +136,20 @@ export class SecureValuesPreprocessor {
function isLogFilterRegex(value: object): value is LogFilterRegex {
return 'pattern' in value;
}
/**
* Transforms the given text to a pattern, which does not match if the
* text is only a part of a longer word.
*
* `\b` only asserts a boundary next to a word character, so it is only added
* to the sides where the text itself begins or ends with one. Otherwise values
* like `P@ssw0rd!` would never match anything.
*
* @param {string} text The text to match
* @returns {string} The resulting regular expression source
*/
function toExactMatchPattern(text: string): string {
const prefix = /^\w/.test(text) ? '\\b' : '';
const suffix = /\w$/.test(text) ? '\\b' : '';
return `${prefix}${escapeRegExp(text)}${suffix}`;
}
@@ -59,6 +59,28 @@ describe('Log Internals', function () {
expect(preprocessor.preprocess(':yolo" yo Yolo yyolo')).to.eql(`:${replacer}" yo ${replacer} yyolo`);
});
it('should preprocess values starting or ending with non-word characters', async function () {
const issues = await preprocessor.loadRules(['P@ssw0rd!', '+15550100', '#hunter2']);
expect(issues.length).to.eql(0);
const replacer = preprocessor.rules[0].replacer;
expect(preprocessor.preprocess('P@ssw0rd! call +15550100 tag #hunter2')).to.eql(
`${replacer} call ${replacer} tag ${replacer}`,
);
});
it(`should preprocess a value with non-word characters given as 'text'`, async function () {
const issues = await preprocessor.loadRules([{text: '$ecret'}]);
expect(issues.length).to.eql(0);
const replacer = preprocessor.rules[0].replacer;
expect(preprocessor.preprocess('the token is $ecret')).to.eql(`the token is ${replacer}`);
});
it('should not preprocess a value which is a part of a longer word', async function () {
const issues = await preprocessor.loadRules(['yolo']);
expect(issues.length).to.eql(0);
expect(preprocessor.preprocess('yolos yyolo')).to.eql('yolos yyolo');
});
it('should leave the string unchanged if all rules have issues', async function () {
const replacer2 = '***';
const issues = await preprocessor.loadRules([null, {flags: 'i'}, {pattern: '^:(', replacer: replacer2}] as any);