Skip to content

Commit 3e9f251

Browse files
Improve URL sanitization to support relative URLs
1 parent 01a1f3d commit 3e9f251

File tree

3 files changed

+79
-23
lines changed

3 files changed

+79
-23
lines changed

sdk/highlight-run/src/client/listeners/network-listener/utils/network-sanitizer.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,27 @@ const SENSITIVE_QUERY_PARAMS = [
7676
'x-goog-signature',
7777
]
7878

79+
/**
80+
* Safely parses a URL, handling both absolute and relative URLs.
81+
* For relative URLs, resolves against globalThis.location.origin (browser/worker)
82+
* or a placeholder base (non-browser environments).
83+
*/
84+
export const safeParseUrl = (url: string): URL => {
85+
try {
86+
return new URL(url)
87+
} catch {
88+
// For relative URLs, we need a base to parse. The base doesn't affect
89+
// the output since sanitizeUrl strips it for relative URLs.
90+
// Use globalThis for broader environment support (window, workers, etc.)
91+
return new URL(url, globalThis.location?.origin ?? 'http://example.com')
92+
}
93+
}
94+
7995
/**
8096
* Sanitizes a URL according to OpenTelemetry semantic conventions.
8197
* - Redacts credentials (username:password) in the URL
8298
* - Redacts sensitive query parameter values while preserving keys
99+
* - Handles both absolute and relative URLs
83100
*
84101
* @param url - The URL string to sanitize
85102
* @returns Sanitized URL string
@@ -91,12 +108,15 @@ const SENSITIVE_QUERY_PARAMS = [
91108
* @example
92109
* sanitizeUrl('https://example.com/path?color=blue&sig=secret123')
93110
* // Returns: 'https://example.com/path?color=blue&sig=REDACTED'
111+
*
112+
* @example
113+
* sanitizeUrl('/api?sig=secret123')
114+
* // Returns: '/api?sig=REDACTED'
94115
*/
95116
export const sanitizeUrl = (url: string): string => {
96117
try {
97-
const urlObject = new URL(url)
118+
const urlObject = safeParseUrl(url)
98119

99-
// Redact credentials if present
100120
if (urlObject.username || urlObject.password) {
101121
urlObject.username = 'REDACTED'
102122
urlObject.password = 'REDACTED'
@@ -111,10 +131,13 @@ export const sanitizeUrl = (url: string): string => {
111131
}
112132
})
113133

134+
// If the URL is relative, return only the pathname + search + hash
135+
if (!url.includes('://')) {
136+
return urlObject.pathname + urlObject.search + urlObject.hash
137+
}
138+
114139
return urlObject.toString()
115140
} catch {
116-
// If URL parsing fails, return original URL
117-
// This handles relative URLs or malformed URLs
118141
return url
119142
}
120143
}

sdk/highlight-run/src/client/otel/index.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as SemanticAttributes from '@opentelemetry/semantic-conventions'
2727
import { getResponseBody } from '../listeners/network-listener/utils/fetch-listener'
2828
import {
2929
DEFAULT_URL_BLOCKLIST,
30+
safeParseUrl,
3031
sanitizeHeaders,
3132
sanitizeUrl,
3233
} from '../listeners/network-listener/utils/network-sanitizer'
@@ -481,23 +482,6 @@ export const shutdown = async () => {
481482
])
482483
}
483484

484-
/**
485-
* Safely parses a URL, handling both absolute and relative URLs.
486-
* For relative URLs, resolves against window.location.origin (browser)
487-
* or a placeholder base (non-browser environments).
488-
*/
489-
export const safeParseUrl = (url: string): URL => {
490-
try {
491-
return new URL(url)
492-
} catch {
493-
if (typeof window !== 'undefined') {
494-
return new URL(url, window.location.origin)
495-
}
496-
497-
return new URL(url, 'http://localhost')
498-
}
499-
}
500-
501485
const enhanceSpanWithHttpRequestAttributes = (
502486
span: api.Span,
503487
body: Request['body'] | RequestInit['body'] | BrowserXHR['_body'],

sdk/highlight-run/src/client/otel/instrumentation.test.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { describe, it, expect } from 'vitest'
22
import {
3+
safeParseUrl,
34
sanitizeHeaders,
45
sanitizeUrl,
56
} from '../listeners/network-listener/utils/network-sanitizer'
67
import {
78
parseXhrResponseHeaders,
89
splitHeaderValue,
910
convertHeadersToOtelAttributes,
10-
safeParseUrl,
1111
} from './index'
1212

1313
describe('Network Instrumentation Custom Attributes', () => {
@@ -1224,7 +1224,8 @@ describe('Network Instrumentation Custom Attributes', () => {
12241224
})
12251225

12261226
it('should return original URL if parsing fails', () => {
1227-
const invalidUrl = 'not-a-valid-url'
1227+
// Truly malformed URLs that fail even with a base URL fallback
1228+
const invalidUrl = 'http://[invalid-ipv6'
12281229
const result = sanitizeUrl(invalidUrl)
12291230
expect(result).toBe(invalidUrl)
12301231
})
@@ -1254,6 +1255,54 @@ describe('Network Instrumentation Custom Attributes', () => {
12541255
expect(result).toContain('sig=REDACTED')
12551256
})
12561257
})
1258+
1259+
describe('relative URLs', () => {
1260+
it('should redact sensitive query params in relative URLs', () => {
1261+
const url = '/api?sig=secret'
1262+
const result = sanitizeUrl(url)
1263+
expect(result).toBe('/api?sig=REDACTED')
1264+
})
1265+
1266+
it('should redact AWSAccessKeyId in relative URLs', () => {
1267+
const url =
1268+
'/api?awsAccessKeyId=AKIAIOSFODNN7EXAMPLE&color=blue'
1269+
const result = sanitizeUrl(url)
1270+
expect(result).toBe('/api?awsAccessKeyId=REDACTED&color=blue')
1271+
})
1272+
1273+
it('should redact multiple sensitive params in relative URLs', () => {
1274+
const url =
1275+
'/path/to/resource?signature=abc123&x-goog-signature=xyz789'
1276+
const result = sanitizeUrl(url)
1277+
expect(result).toBe(
1278+
'/path/to/resource?signature=REDACTED&x-goog-signature=REDACTED',
1279+
)
1280+
})
1281+
1282+
it('should handle relative URLs without query params', () => {
1283+
const url = '/api/data'
1284+
const result = sanitizeUrl(url)
1285+
expect(result).toBe('/api/data')
1286+
})
1287+
1288+
it('should handle relative URLs with fragment', () => {
1289+
const url = '/api?sig=secret#section'
1290+
const result = sanitizeUrl(url)
1291+
expect(result).toBe('/api?sig=REDACTED#section')
1292+
})
1293+
1294+
it('should handle relative URLs with safe query params only', () => {
1295+
const url = '/users?id=123&filter=active'
1296+
const result = sanitizeUrl(url)
1297+
expect(result).toBe('/users?id=123&filter=active')
1298+
})
1299+
1300+
it('should handle root-relative URLs', () => {
1301+
const url = '/?sig=secret'
1302+
const result = sanitizeUrl(url)
1303+
expect(result).toBe('/?sig=REDACTED')
1304+
})
1305+
})
12571306
})
12581307

12591308
describe('safeParseUrl', () => {

0 commit comments

Comments
 (0)