fix: sanitize diagnostics log edge cases

This commit is contained in:
Gustavo Madeira Santana
2026-04-22 16:46:29 -04:00
parent 7cdaa92b3f
commit 916ae146d4
3 changed files with 24 additions and 6 deletions

View File

@@ -148,6 +148,12 @@ describe("diagnostic support export", () => {
},
time: "2026-04-22T12:00:00.100Z",
}),
JSON.stringify({
time: "2026-04-22T12:00:00.200Z",
level: "info",
component: "gateway/server",
msg: "user said structured secret payload",
}),
JSON.stringify({
"0": JSON.stringify({ subsystem: "gateway/channels/matrix" }),
"1": privateChat,
@@ -156,7 +162,7 @@ describe("diagnostic support export", () => {
name: "gateway-runtime",
hostname: "support-host",
},
time: "2026-04-22T12:00:00.200Z",
time: "2026-04-22T12:00:00.300Z",
}),
`plain fallback ${privateChat} ${fakeToken}`,
],
@@ -246,6 +252,7 @@ describe("diagnostic support export", () => {
expect(combined).not.toContain(os.hostname());
expect(combined).not.toContain("QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
expect(combined).not.toContain("sid=secret");
expect(combined).not.toContain("structured secret payload");
expect(combined).not.toContain(fakeAwsKey);
expect(combined).not.toContain(fakeJwt);
expect(combined).toContain("payload.large");
@@ -377,6 +384,7 @@ describe("diagnostic support export", () => {
"connect wss://support-user:support-password@gateway.example/ws?token=short-token&ok=1",
"connect wss://<redacted>:<redacted>@gateway.example/ws?token=<redacted>",
],
["connect https://token@gateway.example/ws", "connect https://<redacted>@gateway.example/ws"],
["auth Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", "auth Basic <redacted>"],
["Cookie: sid=secret; theme=light", "Cookie: <redacted>"],
[`aws ${fakeAwsKey}`, "aws <redacted-aws-key>"],

View File

@@ -272,6 +272,7 @@ function sanitizeConfigDetails(parsed: unknown, redaction: SupportRedactionConte
function configShapeReadFailure(params: {
configPath: string;
redaction: SupportRedactionContext;
stat?: fs.Stats;
error?: string;
}): ConfigShape {
@@ -286,7 +287,7 @@ function configShapeReadFailure(params: {
shape.mtime = params.stat.mtime.toISOString();
}
if (params.error) {
shape.error = redactTextForSupport(params.error);
shape.error = redactSupportString(params.error, params.redaction);
}
return shape;
}
@@ -300,7 +301,7 @@ function readConfigExport(options: {
const stat = fs.existsSync(options.configPath) ? fs.statSync(options.configPath) : null;
if (!stat) {
return {
shape: configShapeReadFailure({ configPath: redactedConfigPath }),
shape: configShapeReadFailure({ configPath: redactedConfigPath, redaction: options }),
};
}
try {
@@ -309,6 +310,7 @@ function readConfigExport(options: {
return {
shape: configShapeReadFailure({
configPath: redactedConfigPath,
redaction: options,
stat,
error: parsed.error,
}),
@@ -322,6 +324,7 @@ function readConfigExport(options: {
return {
shape: configShapeReadFailure({
configPath: redactedConfigPath,
redaction: options,
stat,
error: error instanceof Error ? error.message : String(error),
}),
@@ -541,7 +544,12 @@ function addSafeLogField(
return;
}
if (typeof value === "string") {
sanitized[key] = sanitizeLogString(value, redaction);
const message = sanitizeLogString(value, redaction);
if (key === "msg" && (!message || UNSAFE_LOG_MESSAGE_RE.test(message))) {
addOmittedLogMessageMetadata(sanitized, value);
return;
}
sanitized[key] = message;
} else if (typeof value === "number" || typeof value === "boolean" || value === null) {
sanitized[key] = value;
}

View File

@@ -16,7 +16,7 @@ const BASIC_AUTH_RE = /\bBasic\s+[A-Za-z0-9+/]+={0,2}/giu;
const COOKIE_HEADER_RE = /\b(Cookie|Set-Cookie)\s*:\s*[^\r\n]+/giu;
const AWS_ACCESS_KEY_ID_RE = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu;
const JWT_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu;
const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/@\s:?#]+):([^/@\s?#]+)@/giu;
const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/@\s:?#]+)(?::([^/@\s?#]+))?@/giu;
const SENSITIVE_URL_PARAM_RE =
/([?&](?:api[-_]?key|access[-_]?token|auth[-_]?token|hook[-_]?token|password|passwd|refresh[-_]?token|secret|token)=)[^&#\s]+/giu;
const EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/giu;
@@ -140,7 +140,9 @@ function redactCommonCredentialTextForSupport(value: string): string {
function redactUrlSecretsForSupport(value: string): string {
return value
.replace(URL_USERINFO_RE, "$1<redacted>:<redacted>@")
.replace(URL_USERINFO_RE, (_match, scheme: string, _username: string, password?: string) =>
password ? `${scheme}<redacted>:<redacted>@` : `${scheme}<redacted>@`,
)
.replace(SENSITIVE_URL_PARAM_RE, "$1<redacted>");
}