Skip to content

Commit d55c4a5

Browse files
authored
fix: URLs in error meta-messages (#4564)
fix: strip basic-auth credentials from URLs in error meta-messages Previously, errors raised by HttpRequestError, WebSocketRequestError, RpcRequestError, and TimeoutError formatted the request URL verbatim in their meta-messages — which leaked basic-auth credentials when an RPC URL of the form 'https://user:pass@host' was used. 'getUrl' (the single chokepoint shared by every URL-bearing error formatter in src/errors/request.ts) is now a sanitizer: it parses the URL and clears 'username'/'password' before returning, falling back to the raw input on parse failure. Amp-Thread-ID: https://ampcode.com/threads/T-019de09d-2011-716e-a2cc-bf98f248520d
1 parent 08d9ced commit d55c4a5

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"viem": patch
3+
---
4+
5+
Stripped basic-auth credentials (`user:pass@`) from URLs surfaced in
6+
error meta-messages (`HttpRequestError`, `WebSocketRequestError`,
7+
`RpcRequestError`, `TimeoutError`).

src/errors/utils.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expect, test } from 'vitest'
2+
3+
import { getUrl } from './utils.js'
4+
5+
test('passes a credential-free URL through unchanged', () => {
6+
expect(getUrl('https://example.com/rpc')).toMatchInlineSnapshot(
7+
`"https://example.com/rpc"`,
8+
)
9+
})
10+
11+
test('strips username + password', () => {
12+
expect(getUrl('https://user:pass@example.com/rpc')).toMatchInlineSnapshot(
13+
`"https://example.com/rpc"`,
14+
)
15+
})
16+
17+
test('strips a username-only credential', () => {
18+
expect(getUrl('https://user@example.com/rpc')).toMatchInlineSnapshot(
19+
`"https://example.com/rpc"`,
20+
)
21+
})
22+
23+
test('strips a password-only credential', () => {
24+
expect(getUrl('https://:pass@example.com/rpc')).toMatchInlineSnapshot(
25+
`"https://example.com/rpc"`,
26+
)
27+
})
28+
29+
test('preserves query string and hash after stripping', () => {
30+
expect(
31+
getUrl('https://user:pass@example.com/rpc?key=value#frag'),
32+
).toMatchInlineSnapshot(`"https://example.com/rpc?key=value#frag"`)
33+
})
34+
35+
test('returns the input untouched when not a parseable URL', () => {
36+
expect(getUrl('not-a-url')).toMatchInlineSnapshot(`"not-a-url"`)
37+
})

src/errors/utils.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,20 @@ import type { Address } from 'abitype'
33
export type ErrorType<name extends string = 'Error'> = Error & { name: name }
44

55
export const getContractAddress = (address: Address) => address
6-
export const getUrl = (url: string) => url
6+
7+
/**
8+
* Returns the URL with any embedded basic-auth credentials stripped, so
9+
* error messages and logs don't leak secrets when an RPC URL like
10+
* `https://user:pass@host` is used.
11+
*/
12+
export const getUrl = (url: string) => {
13+
try {
14+
const parsed = new URL(url)
15+
if (!parsed.username && !parsed.password) return url
16+
parsed.username = ''
17+
parsed.password = ''
18+
return parsed.toString()
19+
} catch {
20+
return url
21+
}
22+
}

0 commit comments

Comments
 (0)