Skip to content

Commit a25e846

Browse files
rkolesnikovDXRoman I. Kolesnikov false
andauthored
Updated rules for telemetry cleanup (#2817)
* Updated rules for telemetry cleanup * Additional check --------- Co-authored-by: Roman I. Kolesnikov false <rokolesnikov@microsoft.com>
1 parent c6ee7ab commit a25e846

File tree

5 files changed

+303
-221
lines changed

5 files changed

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

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)