-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassert-safe.ts
More file actions
66 lines (62 loc) · 2.23 KB
/
assert-safe.ts
File metadata and controls
66 lines (62 loc) · 2.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @file SSRF guard for operator- or issuer-supplied URLs — `assertSafeHttpUrl`
* parses a raw URL, rejects non-HTTP(S) schemes, and refuses hosts that
* resolve to loopback / private / link-local ranges (cloud metadata, redis,
* internal services). A server that fetches a URL it did not author (an OAuth
* issuer, an introspection endpoint advertised in its metadata, a webhook
* target) runs the candidate through this before the request leaves the box.
*/
import { isLoopbackHost, isPrivateHost } from './predicates'
import type { AssertSafeHttpUrlOptions } from './types'
const UrlCtor = URL
/**
* Parse `rawUrl` and assert it is safe to fetch server-side, returning the
* parsed `URL`. Throws when the value does not parse, uses a scheme other than
* `http:` / `https:`, or resolves to a loopback / private / link-local host.
* Set `allowLocalhost` to permit `localhost` / `127.0.0.1` / `::1` for
* local-stack development. `label` names the subject in the thrown message.
*
* @example
* ;```typescript
* assertSafeHttpUrl('https://api.example.com', { label: 'OAuth issuer' })
* // → URL { href: 'https://api.example.com/' }
*
* assertSafeHttpUrl('http://169.254.169.254/latest/meta-data')
* // → throws: resolves to a private/loopback host
*
* assertSafeHttpUrl('ftp://example.com')
* // → throws: must use http(s)
* ```
*/
export function assertSafeHttpUrl(
rawUrl: string,
options?: AssertSafeHttpUrlOptions | undefined,
): URL {
const { allowLocalhost = false, label = 'URL' } = {
__proto__: null,
...options,
} as AssertSafeHttpUrlOptions
let url: URL
try {
url = new UrlCtor(rawUrl)
} catch {
throw new Error(
`${label} is not a valid URL: ${rawUrl}. Provide an absolute http(s) URL.`,
)
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(
`${label} must use http(s): ${rawUrl}. Got scheme "${url.protocol}"; use http: or https:.`,
)
}
const { hostname } = url
if (allowLocalhost && isLoopbackHost(hostname)) {
return url
}
if (isPrivateHost(hostname)) {
throw new Error(
`${label} resolves to a private/loopback host and is refused: ${rawUrl}. Point it at a public host.`,
)
}
return url
}