Skip to content

Commit

Permalink
handle IPv4/IPv6 addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
JaneJeon committed May 10, 2023
1 parent 5b0f3fc commit 5ecbc54
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 3 deletions.
4 changes: 4 additions & 0 deletions __mocks__/dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ export function lookup(hostname, options, callback) {
) {
callback(undefined, { address: '1.1.1.1', family: 4 })
}

// Really make sure that all test cases - at least, the ones that resolve an actual hostname -
// have a corresponding DNS mock as well (on top of the HTTP mock).
callback(new Error('Could not resolve host'))
}
27 changes: 24 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,34 @@ const protect = async options => {
if (!ALLOWED_PROTOCOLS.includes(options.url.protocol))
throw new Error('Invalid protocol!')

// Another layer of protection against SSRF - ensure we're not hitting internal services
const { address } = await lookup(options.url.hostname)
// Check if the hostname is an IP address - we don't need to "lookup" IP addresses!
let IP
const { hostname } = options.url

if (ip.IPv4.isIPv4(hostname)) {
IP = hostname
} else if (
// Per https://url.spec.whatwg.org/#host-parsing,
// if the hostname starts with a [, we need to check if it ends with a ], and is an IPv6.
hostname.startsWith('[') &&
hostname.endsWith(']') &&
ip.IPv6.isIPv6(hostname.slice(1, -1)) // strip the first and last characters - the brackets
) {
IP = hostname.slice(1, -1)
} else {
// A regular hostname - we need to do a DNS lookup to get the IP address
const { address } = await lookup(hostname)
IP = address
}

// Another layer of protection against SSRF - ensure we're not hitting internal services.
// Try to match "reserved" IP ranges: https://en.wikipedia.org/wiki/Reserved_IP_addresses
// https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html#case-2-application-can-send-requests-to-any-external-ip-address-or-domain-name
// The function returns 'unicast' or the name of the reserved IP range, should it match any.
// This in effect blocks all private IP Range: https://git.io/JWy3u, https://git.io/JWy3b
if (ip.parse(address).range() !== 'unicast')
// We use ip.process() here to deal with potentially IPv4-mapped IPv6 addresses (which will show up as "ipv4mapped"
// and not the whatever range the actual IPv4 address actually belongs to).
if (ip.process(IP).range() !== 'unicast')
throw new Error('The IP of the domain is reserved!')
}

Expand Down
49 changes: 49 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,53 @@ describe('got-ssrf', () => {
)
).rejects.toThrow('The IP of the domain is reserved!')
})

// NOTE: for IP address tests, any valid IP address will be processed directly in the code,
// without the need for a DNS lookup (after all, you do a DNS lookup to get the IP address).
// Therefore, we do not need DNS mocks (__mocks__/dns.js) for the tests below.

it('handles IPv4 addresses', async () => {
// A private IPv4 address
await expect(gotSsrf('http://192.168.0.1')).rejects.toThrow(
'The IP of the domain is reserved!'
)

// A public IPv4 address
nock('http://1.1.1.1').get('/').reply(200)
await gotSsrf('http://1.1.1.1')
})

it('handles IPv6 addresses', async () => {
// This is 127.0.0.1 mapped to IPv6
await expect(gotSsrf('http://[::ffff:7f00:1]:1338/hello')).rejects.toThrow(
'The IP of the domain is reserved!'
)

// A public IPv4 address (1.1.1.1) mapped to IPv6
nock('http://[::ffff:101:101]').get('/').reply(200)
await gotSsrf('http://[::ffff:101:101]')

// A public IPv6 address
nock('http://[2606:2800:220:1:248:1893:25c8:1946]').get('/').reply(200)
await gotSsrf('http://[2606:2800:220:1:248:1893:25c8:1946]')

// A private IPv6 address
await expect(gotSsrf('http://[fe80::ffff:ffff:ffff:ffff]')).rejects.toThrow(
'The IP of the domain is reserved!'
)
})

it('handles hostnames with brackets in it', async () => {
await expect(gotSsrf('http://[hostname1.com')).rejects.toThrow(
'Invalid URL'
)

await expect(gotSsrf('http://[hostnam]e2.com')).rejects.toThrow(
'Invalid URL'
)

await expect(
gotSsrf('http://[2606:2800:220:1:248:1893:25c8:g]')
).rejects.toThrow('Invalid URL')
})
})

0 comments on commit 5ecbc54

Please sign in to comment.