Skip to content

Commit feee16d

Browse files
committed
redact query string parameters on URLs
1 parent 66dac50 commit feee16d

File tree

6 files changed

+86
-12
lines changed

6 files changed

+86
-12
lines changed

packages/plugin-http-errors/http-errors.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const shouldCaptureStatusCode = require('./lib/should-capture-status-code')
1010
const truncate = require('./lib/truncate')
1111
const xhrResponseHeadersToObject = require('./lib/xhr-response-headers-to-object')
1212
const redactValues = require('./lib/redact-values')
13+
const redactQueryParameters = require('./lib/redact-query-parameters')
1314

1415
const DEFAULT_HTTP_ERROR_CODES = [{ min: 400, max: 599 }]
1516
const DEFAULT_MAX_REQUEST_SIZE = 5000
@@ -90,6 +91,10 @@ module.exports = (config = {}, global = window) => {
9091
const method = startContext.method
9192
const domain = extractDomain(url)
9293

94+
// Redact query parameters in URL
95+
const redactedUrl = redactQueryParameters(url, redactedKeys)
96+
const redactedQueryParams = parseQueryParams(redactedUrl)
97+
9398
// Extract request headers
9499
let requestHeaders = {}
95100
if (startContext.xhr && startContext.xhr._requestHeaders) {
@@ -126,10 +131,10 @@ module.exports = (config = {}, global = window) => {
126131

127132
// Create request and response objects for callback
128133
const requestObj = {
129-
url,
134+
url: redactedUrl,
130135
httpMethod: method,
131136
headers: redactValues(requestHeaders, redactedKeys),
132-
params: redactValues(parseQueryParams(url), redactedKeys),
137+
params: redactedQueryParams,
133138
body: requestBody,
134139
bodyLength: requestBodyLength
135140
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const redactValues = require('./redact-values')
2+
const defaultBaseURL = 'http://invalid-base.com'
3+
4+
module.exports = function (url, redactedKeys) {
5+
const urlObj = new URL(url, defaultBaseURL) // base needed for relative URLs
6+
const params = new URLSearchParams(urlObj.search)
7+
const redactedParams = redactValues(Object.fromEntries(params), redactedKeys)
8+
urlObj.search = new URLSearchParams(redactedParams).toString()
9+
const urlString = decodeURI(urlObj.toString())
10+
return urlString.startsWith(defaultBaseURL)
11+
? urlString.slice(defaultBaseURL.length)
12+
: urlString
13+
}

packages/plugin-http-errors/test/http-errors-xhr.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,36 @@ describe('plugin-http-errors', () => {
210210
// since they don't have status codes, but let's verify the behavior
211211
expect(notifyCallbacks.length).toBe(0)
212212
})
213+
214+
it('should redact specified headers and query parameters in XHR URLs', async () => {
215+
const notifyCallbacks: Event[] = []
216+
217+
plugin = createPlugin({
218+
httpErrorCodes: { min: 400, max: 499 }
219+
})
220+
221+
const client = new Client({ apiKey: 'api_key', plugins: [plugin], redactedKeys: ['token', 'userId'] })
222+
client._setDelivery(createMockDelivery(notifyCallbacks))
223+
224+
const xhr = new XMLHttpRequest() as any
225+
xhr.status = 403
226+
xhr.statusText = 'Forbidden'
227+
xhr.response = 'Forbidden'
228+
xhr.responseText = 'Forbidden'
229+
xhr.setRequestHeader('token', 'super-secret-token')
230+
231+
xhr.open('GET', 'https://api.example.com/data?userId=42')
232+
xhr.send()
233+
234+
await new Promise(resolve => setTimeout(resolve, 20))
235+
236+
expect(notifyCallbacks.length).toBe(1)
237+
const event = notifyCallbacks[0].toJSON()
238+
239+
// Verify that sensitive query parameters are redacted
240+
expect(event.request.url).toBe('https://api.example.com/data?userId=[REDACTED]')
241+
expect(event.request.params).toEqual({ userId: '[REDACTED]' })
242+
expect(event.request.headers?.['token']).toBe('[REDACTED]')
243+
})
213244
})
214245
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import redactQueryParameters from '../lib/redact-query-parameters'
2+
3+
describe('redact-query-parameters', () => {
4+
it('redacts specified query parameters in a URL', () => {
5+
const url = 'http://example.com/path?token=abc123&userId=42&status=active'
6+
const redactedKeys = ['token', 'userId']
7+
const redactedUrl = redactQueryParameters(url, redactedKeys)
8+
expect(redactedUrl).toBe('http://example.com/path?token=[REDACTED]&userId=[REDACTED]&status=active')
9+
})
10+
11+
it('handles URLs with no query parameters', () => {
12+
const url = 'http://example.com/path'
13+
const redactedKeys = ['token']
14+
const redactedUrl = redactQueryParameters(url, redactedKeys)
15+
expect(redactedUrl).toBe('http://example.com/path')
16+
})
17+
18+
it('handles relative URLs', () => {
19+
const url = '/path?token=abc123&userId=42'
20+
const redactedKeys = ['token', 'userId']
21+
const redactedUrl = redactQueryParameters(url, redactedKeys)
22+
expect(redactedUrl).toBe('/path?token=[REDACTED]&userId=[REDACTED]')
23+
})
24+
})

test/browser/features/fixtures/http_errors/src/app.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
33
import React, { useEffect } from 'react'
44
import { apiKey, endpoints, plugins, REFLECT_ENDPOINT } from './lib/config'
55

6-
Bugsnag.start({ apiKey, endpoints, plugins, redactedKeys: ['token'] })
6+
Bugsnag.start({ apiKey, endpoints, plugins, redactedKeys: ['token', 'userId'] })
77

88
function App () {
99
useEffect(() => {
@@ -13,20 +13,21 @@ function App () {
1313
switch(type) {
1414
case 'xhr':
1515
const xhr = new XMLHttpRequest()
16-
xhr.open('GET', `${REFLECT_ENDPOINT}?status=404&token=12345`)
16+
xhr.open('GET', `${REFLECT_ENDPOINT}?status=404&userId=12345`)
17+
xhr.setRequestHeader('token', 'super-secret-token')
1718
xhr.send()
1819
break
1920
case 'fetch':
2021
default:
21-
fetch(`${REFLECT_ENDPOINT}?status=404&token=12345`)
22+
fetch(`${REFLECT_ENDPOINT}?status=404&userId=12345`, { headers: { token: 'super-secret-token' }})
2223
break
2324
}
2425

2526
}, [])
2627

2728
return (
2829
<div>
29-
<p>HTTP Errors - fetch</p>
30+
<p>HTTP Errors</p>
3031
</div>
3132
)
3233
}

test/browser/features/http_errors.feature

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Feature: HTTP Errors
99
Then the error is a valid browser payload for the error reporting API
1010

1111
And I define "expected.context" as "GET <browser.hostname>"
12-
And I define "expected.exception.message" as "404: <browser.url>/reflect?status=404"
13-
And I define "expected.request.url" as "<browser.url>/reflect?status=404"
12+
And I define "expected.exception.message" as "404: <browser.url>/reflect?status=404&userId=[REDACTED]"
13+
And I define "expected.request.url" as "<browser.url>/reflect?status=404&userId=[REDACTED]"
1414

1515
And the exception "errorClass" equals "HTTPError"
1616
And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message"
@@ -22,7 +22,7 @@ Feature: HTTP Errors
2222
And the error payload field "events.0.request.url" equals the stored value "expected.request.url"
2323
And the event "request.httpMethod" equals "GET"
2424
And the event "request.params.status" equals "404"
25-
And the event "request.params.token" equals "[REDACTED]"
25+
And the event "request.params.userId" equals "[REDACTED]"
2626
And the event "request.body" equals ""
2727
And the event "request.bodyLength" equals 0
2828

@@ -39,8 +39,8 @@ Feature: HTTP Errors
3939
Then the error is a valid browser payload for the error reporting API
4040

4141
And I define "expected.context" as "GET <browser.hostname>"
42-
And I define "expected.exception.message" as "404: <browser.url>/reflect?status=404"
43-
And I define "expected.request.url" as "<browser.url>/reflect?status=404"
42+
And I define "expected.exception.message" as "404: <browser.url>/reflect?status=404&userId=[REDACTED]"
43+
And I define "expected.request.url" as "<browser.url>/reflect?status=404&userId=[REDACTED]"
4444

4545
And the exception "errorClass" equals "HTTPError"
4646
And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message"
@@ -52,7 +52,7 @@ Feature: HTTP Errors
5252
And the error payload field "events.0.request.url" equals the stored value "expected.request.url"
5353
And the event "request.httpMethod" equals "GET"
5454
And the event "request.params.status" equals "404"
55-
And the event "request.params.token" equals "[REDACTED]"
55+
And the event "request.params.userId" equals "[REDACTED]"
5656
And the event "request.body" equals ""
5757
And the event "request.bodyLength" equals 0
5858

0 commit comments

Comments
 (0)