Skip to content

Commit 35472b1

Browse files
author
Roman I. Kolesnikov false
committed
Updated rules for telemetry cleanup
1 parent c6ee7ab commit 35472b1

File tree

5 files changed

+289
-220
lines changed

5 files changed

+289
-220
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { assert } from "chai";
2+
import { describe, it } from "mocha";
3+
import * as crypto from "crypto";
4+
import { sanitizeUrl, cleanUpUrlParams, cleanUrlSensitiveDataFromQuery, cleanUrlSensitiveDataFromValue } from "./logSanitizer";
5+
6+
describe("sanitizeUrl", () => {
7+
it("should remove sensitive data from query parameters", () => {
8+
const url = "https://example.com/path?client_secret=abc&token=xyz&other=123";
9+
const sanitizedUrl = sanitizeUrl(url);
10+
assert.equal(sanitizedUrl, "https://example.com/path?client_secret=***&token=***&other=123");
11+
});
12+
13+
it("should remove jwt data from query parameters", async () => {
14+
const jwt = await generateTestJwt({ testJwt: true}, "test_secret");
15+
const url = `https://example.com/path?test=${jwt}&token=xyz&other=123`;
16+
const sanitizedUrl = sanitizeUrl(url);
17+
assert.equal(sanitizedUrl, "https://example.com/path?test=***&token=***&other=123");
18+
});
19+
20+
it("should remove sensitive data from hash parameters", () => {
21+
const url = "https://example.com/path#client_secret=abc&token=xyz&other=123";
22+
const sanitizedUrl = sanitizeUrl(url);
23+
assert.equal(sanitizedUrl, "https://example.com/path#client_secret=***&token=***&other=***");
24+
});
25+
26+
it("should handle URLs without sensitive data", () => {
27+
const url = "https://example.com/path?other=123";
28+
const sanitizedUrl = sanitizeUrl(url);
29+
assert.equal(sanitizedUrl, "https://example.com/path?other=123");
30+
});
31+
32+
it("should handle URLs with only allowed parameters in hash", () => {
33+
const url = "https://example.com/path#state=abc&session_state=xyz&client_secret=abc";
34+
const sanitizedUrl = sanitizeUrl(url);
35+
assert.equal(sanitizedUrl, "https://example.com/path#state=abc&session_state=xyz&client_secret=***");
36+
});
37+
38+
it("should handle null or undefined URLs", () => {
39+
assert.equal(sanitizeUrl(null), null);
40+
assert.equal(sanitizeUrl(undefined), undefined);
41+
});
42+
43+
it("should handle empty URLs", () => {
44+
assert.equal(sanitizeUrl(""), "");
45+
});
46+
});
47+
48+
49+
describe("cleanUpUrlParams", () => {
50+
it("should replace sensitive parameters with ***", () => {
51+
const url = "https://example.com/path#client_secret=abc&token=xyz&other=123";
52+
const cleanedUrl = cleanUpUrlParams(url);
53+
assert.equal(cleanedUrl, "https://example.com/path#client_secret=***&token=***&other=***");
54+
});
55+
56+
it("should leave allowed parameters unchanged", () => {
57+
const url = "https://example.com/path#state=abc&session_state=xyz&client_secret=abc";
58+
const cleanedUrl = cleanUpUrlParams(url);
59+
assert.equal(cleanedUrl, "https://example.com/path#state=abc&session_state=xyz&client_secret=***");
60+
});
61+
62+
it("should handle URLs without hash", () => {
63+
const url = "https://example.com/path";
64+
const cleanedUrl = cleanUpUrlParams(url);
65+
assert.equal(cleanedUrl, "https://example.com/path");
66+
});
67+
68+
it("should handle null or undefined URLs", () => {
69+
assert.equal(cleanUpUrlParams(null), null);
70+
assert.equal(cleanUpUrlParams(undefined), undefined);
71+
});
72+
73+
it("should handle empty URLs", () => {
74+
assert.equal(cleanUpUrlParams(""), "");
75+
});
76+
});
77+
78+
describe("cleanUrlSensitiveDataFromQuery", () => {
79+
it("should replace sensitive query parameters with ***", () => {
80+
const url = "https://example.com/path?client_secret=abc&token=xyz&other=123";
81+
const cleanedUrl = cleanUrlSensitiveDataFromQuery(url);
82+
assert.equal(cleanedUrl, "https://example.com/path?client_secret=***&token=***&other=123");
83+
});
84+
85+
it("should handle URLs without query parameters", () => {
86+
const url = "https://example.com/path";
87+
const cleanedUrl = cleanUrlSensitiveDataFromQuery(url);
88+
assert.equal(cleanedUrl, "https://example.com/path");
89+
});
90+
91+
it("should handle null or undefined URLs", () => {
92+
assert.equal(cleanUrlSensitiveDataFromQuery(null), null);
93+
assert.equal(cleanUrlSensitiveDataFromQuery(undefined), undefined);
94+
});
95+
96+
it("should handle empty URLs", () => {
97+
assert.equal(cleanUrlSensitiveDataFromQuery(""), "");
98+
});
99+
100+
it("should handle complex URLs with multiple parameters", () => {
101+
const url = "https://example.com/api/v1?client_secret=abc123&api_key=xyz789&user=john&password=pass123&normal=value";
102+
const cleanedUrl = cleanUrlSensitiveDataFromQuery(url);
103+
assert.equal(cleanedUrl, "https://example.com/api/v1?client_secret=***&api_key=xyz789&user=***&password=***&normal=value");
104+
});
105+
106+
it("should handle URLs with encoded characters", () => {
107+
const url = "https://example.com/path?token=abc%26xyz&user_name=john%20doe";
108+
const cleanedUrl = cleanUrlSensitiveDataFromQuery(url);
109+
assert.equal(cleanedUrl, "https://example.com/path?token=***&user_name=***");
110+
});
111+
112+
it("should handle special cases like access_token and user_name", () => {
113+
const url = "https://example.com/oauth?access_token=abc123&user_name=john";
114+
const cleanedUrl = cleanUrlSensitiveDataFromQuery(url);
115+
assert.equal(cleanedUrl, "https://example.com/oauth?access_token=***&user_name=***");
116+
});
117+
118+
it("should handle malformed URLs by using fallback mechanism", () => {
119+
const url = "invalid://url with spaces?token=abc";
120+
const cleanedUrl = cleanUrlSensitiveDataFromQuery(url);
121+
// Should still sanitize using regex fallback
122+
assert.equal(cleanedUrl, "invalid://url with spaces?token=***");
123+
});
124+
});
125+
126+
describe("cleanUrlSensitiveDataFromValue", () => {
127+
it("should replace sensitive data in header values with ***", () => {
128+
const dataValue = "client_secret=abc&token=xyz&other=123";
129+
const cleanedValue = cleanUrlSensitiveDataFromValue(dataValue);
130+
assert.equal(cleanedValue, "client_secret=***&token=***&other=123");
131+
});
132+
133+
it("should replace jwt data in header values with ***", async () => {
134+
const dataValue = `test=${await generateTestJwt({ testJwt: true}, "test_secret")}&token=xyz&other=123`;
135+
const cleanedValue = cleanUrlSensitiveDataFromValue(dataValue);
136+
assert.equal(cleanedValue, "test=***&token=***&other=123");
137+
});
138+
139+
it("should handle values without sensitive data", () => {
140+
const dataValue = "other=123";
141+
const cleanedValue = cleanUrlSensitiveDataFromValue(dataValue);
142+
assert.equal(cleanedValue, "other=123");
143+
});
144+
145+
it("should handle null or undefined values", () => {
146+
assert.equal(cleanUrlSensitiveDataFromValue(null), null);
147+
assert.equal(cleanUrlSensitiveDataFromValue(undefined), undefined);
148+
});
149+
150+
it("should handle empty values", () => {
151+
assert.equal(cleanUrlSensitiveDataFromValue(""), "");
152+
});
153+
});
154+
155+
async function generateTestJwt(payload, secret, header = { alg: "HS256", typ: "JWT" }): Promise<string> {
156+
const headerEncoded = stringToBase64Url(JSON.stringify(header));
157+
const payloadEncoded = stringToBase64Url(JSON.stringify(payload));
158+
159+
const dataToSignString = `${headerEncoded}.${payloadEncoded}`;
160+
161+
const encoder = new TextEncoder();
162+
const secretKeyData = encoder.encode(secret); // Secret is a string, convert to Uint8Array
163+
const dataToSign = encoder.encode(dataToSignString);
164+
165+
const cryptoKey = await crypto.subtle.importKey(
166+
"raw", // format: raw key data
167+
secretKeyData, // keyData: Uint8Array of the secret
168+
{ name: "HMAC", hash: "SHA-256" }, // algorithm details
169+
false, // extractable: whether the key can be exported
170+
["sign"] // keyUsages: "sign" for HMAC
171+
);
172+
173+
const signatureBuffer = await crypto.subtle.sign(
174+
"HMAC", // algorithm name
175+
cryptoKey, // CryptoKey for signing
176+
dataToSign // Data to sign as ArrayBuffer or TypedArray
177+
);
178+
179+
const signatureEncoded = arrayBufferToBase64Url(signatureBuffer);
180+
181+
return `${dataToSignString}.${signatureEncoded}`;
182+
183+
function stringToBase64Url(str) {
184+
const encoder = new TextEncoder();
185+
const uint8Array = encoder.encode(str);
186+
let binaryString = '';
187+
uint8Array.forEach(byte => {
188+
binaryString += String.fromCharCode(byte);
189+
});
190+
return btoa(binaryString)
191+
.replace(/\+/g, '-')
192+
.replace(/\//g, '_')
193+
.replace(/=+$/, '');
194+
}
195+
196+
// Helper function to Base64URL encode an ArrayBuffer
197+
function arrayBufferToBase64Url(buffer) {
198+
let binary = '';
199+
const bytes = new Uint8Array(buffer);
200+
const len = bytes.byteLength;
201+
for (let i = 0; i < len; i++) {
202+
binary += String.fromCharCode(bytes[i]);
203+
}
204+
return btoa(binary)
205+
.replace(/\+/g, '-')
206+
.replace(/\//g, '_')
207+
.replace(/=+$/, '');
208+
}
209+
}

src/logging/utils/logSanitizer.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const sensitiveParams = ["client_secret", "salt", "sig", "signature", "key", "secret", "token", "access_token", "username", "user_name", "user", "password"];
2+
const allowedList = new Set(["state", "session_state"]);
3+
4+
export function sanitizeUrl(requestUrl: string): string {
5+
if (!requestUrl) {
6+
return requestUrl;
7+
}
8+
const url = requestUrl;
9+
10+
// Clean hash parameters if they exist
11+
if (url.match(/#.*=/)) {
12+
return cleanUpUrlParams(url);
13+
} else {
14+
return cleanUrlSensitiveDataFromQuery(url);
15+
}
16+
}
17+
18+
export function cleanUpUrlParams(requestUrl: string): string {
19+
if (!requestUrl) {
20+
return requestUrl;
21+
}
22+
try {
23+
const url = new URL(requestUrl);
24+
const hash = url.hash.substring(1); // Remove the leading '#'
25+
const params = new URLSearchParams(hash);
26+
27+
// Remove all parameters except those in the allowedList
28+
for (const key of params.keys()) {
29+
if (!allowedList.has(key)) {
30+
// Replace the 'code' parameter value
31+
params.set(key, "***");
32+
}
33+
}
34+
35+
url.hash = params.toString();
36+
return url.toString();
37+
} catch (e) {
38+
// Fallback to empty string if URL parsing fails
39+
return "";
40+
}
41+
}
42+
43+
export function cleanUrlSensitiveDataFromQuery(requestUrl: string): string {
44+
if (requestUrl) {
45+
requestUrl = requestUrl.replace(/([?|&])(client_secret|salt|sig|signature|key|secret|(access_)?token|user(_)?(name)?|password)=([^&]+)/ig, "$1$2=***");
46+
requestUrl = requestUrl.replace(/(eyJ[a-z0-9\\-_%]+\.eyJ[^&]*)/ig, "***");
47+
48+
// Parse the URL to handle the query parameters correctly
49+
try {
50+
const url = new URL(requestUrl);
51+
const params = new URLSearchParams(url.search);
52+
53+
sensitiveParams.forEach(param => {
54+
if (params.has(param)) {
55+
params.set(param, "***");
56+
}
57+
});
58+
59+
url.search = params.toString();
60+
return url.toString();
61+
} catch (e) {
62+
// Fallback to the current implementation if URL parsing fails
63+
return requestUrl;
64+
}
65+
}
66+
return requestUrl;
67+
}
68+
69+
export function cleanUrlSensitiveDataFromValue(dataValue: string): string {
70+
if (dataValue) {
71+
dataValue = dataValue.replace(/((client_secret|salt|sig|signature|secret|(access_)?token|user(_)?(name)?|password))=([^&]+)/ig, "$1$3=***");
72+
dataValue = dataValue.replace(/(eyJ[a-z0-9\\-_%]+\.[^&]*)/ig, "***");
73+
}
74+
return dataValue;
75+
}

0 commit comments

Comments
 (0)