Socket
Socket
Sign inDemoInstall

got-ssrf

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

got-ssrf - npm Package Compare versions

Comparing version 1.3.5 to 2.0.0

coverage/clover.xml

6

index.js

@@ -10,4 +10,2 @@ import { lookup as nativeCallbackLookup } from 'dns'

const ALLOWED_PROTOCOLS = ['http:', 'https:']
// Assume all URLs are properly formed by the time it hits the hooks

@@ -30,4 +28,4 @@ const protect = async options => {

// that are within the same network but is not intended to be reached by the user.
if (!ALLOWED_PROTOCOLS.includes(options.url.protocol))
throw new Error('Invalid protocol!')
// This is done automatically by got, so we don't need to do anything here:
// https://github.com/sindresorhus/got/blob/8f77e8d07d8684cde95d351feafaa308b466dff4/source/core/options.ts#L1411

@@ -34,0 +32,0 @@ // Check if the hostname is an IP address - we don't need to "lookup" IP addresses!

import { expect, describe, it, jest } from '@jest/globals'
import { promisify } from 'util'
import nock from 'nock'
import CacheableLookup from 'cacheable-lookup'

@@ -14,80 +16,190 @@ // We can directly mock the "import { lookup } from 'dns'" call in index.js with jest.

// Whether you pass in dnsCache: true, or an *instance* of CacheableLookup,
// the dnsCache.lookupAsync being used is from an instance of the CacheableLookup class,
// so we don't have to separately test the { dnsCache: true } case.
// We just need to ensure that this library works correctly with a dnsCache.lookupAsync.
const dnsCache = new CacheableLookup()
// Also, for some reason trying to pass a mocked resolver with a resolve4() and a resolve6()
// that always throws ENOTFOUND doesn't work (i.e. it doesn't use the `lookup` we pass to the options).
// So we just directly mock lookupAsync.
jest
.spyOn(dnsCache, 'lookupAsync')
.mockImplementation(promisify(mockDnsModule.lookup))
const setups = [
{
title: 'dnsCache',
options: {
dnsCache
}
},
{
title: 'dnsLookup',
options: {
dnsLookup: mockDnsModule.lookup
}
},
{
title: 'native dns',
options: {}
}
]
describe('got-ssrf', () => {
it('works for public address', async () => {
nock('http://public-url.com').get('/').reply(200)
await gotSsrf('http://public-url.com/')
})
describe.each(setups)('w/ $title', ({ options }) => {
const got = gotSsrf.extend(options)
it('throws for reserved addresses', async () => {
nock('http://private-url.com').get('/').reply(200)
await expect(gotSsrf('http://private-url.com/')).rejects.toThrow(
'The IP of the domain is reserved!'
)
})
it('rejects non-http(s) protocols', async () => {
await expect(got('ftp://example.com')).rejects.toThrow(
'Unsupported protocol: ftp'
)
it('checks every redirect', async () => {
nock(
'http://public-url-that-redirects-to-private-url-that-redirects-to-public-url.com'
)
.get('/')
.reply(301, 'Moved', { Location: 'http://private-url.com/' })
nock('http://private-url.com')
.get('/')
.reply(301, 'Moved', { Location: 'http://public-url.com/' })
nock('http://public-url.com').get('/').reply(200)
await expect(
gotSsrf(
await expect(got('http2://example.com')).rejects.toThrow(
'Unsupported protocol: http2'
)
await expect(got('file:///etc/passwd')).rejects.toThrow(
'Unsupported protocol: file'
)
// You *need* to specify the protocol
await expect(got('example.com')).rejects.toThrow('Invalid URL')
})
it('works for public address', async () => {
nock('http://public-url.com').get('/').reply(200)
await got('http://public-url.com/')
})
it('throws for reserved addresses', async () => {
nock('http://private-url.com').get('/').reply(200)
await expect(got('http://private-url.com/')).rejects.toThrow(
'The IP of the domain is reserved!'
)
})
it('checks every redirect', async () => {
// Basically, we prevent "smuggling" internal endpoints from a public hostname
// by checking the URL before every redirect.
// In this example, the seemingly public URL redirects to private-url.com,
// so even though the private-url.com ultimately redirects the URL to a public one,
// we must still reject this request!
nock(
'http://public-url-that-redirects-to-private-url-that-redirects-to-public-url.com'
)
).rejects.toThrow('The IP of the domain is reserved!')
})
.get('/')
.reply(301, 'Moved', { Location: 'http://private-url.com/' })
nock('http://private-url.com')
.get('/')
.reply(301, 'Moved', { Location: 'http://public-url.com/' })
nock('http://public-url.com').get('/').reply(200)
await expect(
got(
'http://public-url-that-redirects-to-private-url-that-redirects-to-public-url.com'
)
).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 weird URLs/edge cases', async () => {
await expect(got('http://public-url.com.')).rejects.toThrow(
'The IP of the domain is reserved!'
)
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!'
)
await expect(got('http://example.com:foo')).rejects.toThrow('Invalid URL')
// A public IPv4 address
nock('http://1.1.1.1').get('/').reply(200)
await gotSsrf('http://1.1.1.1')
})
// Below are trick cases from https://azeemba.com/posts/what-is-a-url.html#query-or-username
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!'
)
// Based on the http://http://http://@http://http://?http://#http:// example.
await expect(
got('http://private://part2://@part3://part4://?part5://#part6://')
).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]')
// Query or Username?
await expect(got('http://1.1.1.1 &@ 2.2.2.2# @3.3.3.3/')).rejects.toThrow(
'Invalid URL'
)
// 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]')
await expect(got('http://1.1.1.1&@127.0.0.1#@3.3.3.3/')).rejects.toThrow(
'The IP of the domain is reserved!'
)
// A private IPv6 address
await expect(gotSsrf('http://[fe80::ffff:ffff:ffff:ffff]')).rejects.toThrow(
'The IP of the domain is reserved!'
)
})
// Port or Path?
await expect(got('http://127.0.0.1:5000:80/')).rejects.toThrow(
'Invalid URL'
)
it('handles hostnames with brackets in it', async () => {
await expect(gotSsrf('http://[hostname1.com')).rejects.toThrow(
'Invalid URL'
)
// Host confusion (see: https://daniel.haxx.se/blog/2021/04/19/curl-those-funny-ipv4-addresses/)
await expect(got('http://127.0.1')).rejects.toThrow(
'The IP of the domain is reserved!' // the first number assumed to be 8 bits, the next 8, then 16
)
await expect(got('http://127.1')).rejects.toThrow(
'The IP of the domain is reserved!' // the first number assumed to be 8 bits, the next one 24
)
await expect(got('http://2130706433')).rejects.toThrow(
'The IP of the domain is reserved!' // 32-bit number converted as IPv4 addresses
)
await expect(got('http://0300.0250.0.01')).rejects.toThrow(
'The IP of the domain is reserved!' // zero-prefix = octal number -> converted to 192.168.0.1
)
await expect(got('http://0xc0.0xa8.0x00.0x01')).rejects.toThrow(
'The IP of the domain is reserved!' // same deal, but octal
)
await expect(gotSsrf('http://[hostnam]e2.com')).rejects.toThrow(
'Invalid URL'
)
// Other weird hostnames
await expect(got('http://example.com%2F10.0.0.1/')).rejects.toThrow(
'Invalid URL'
)
})
await expect(
gotSsrf('http://[2606:2800:220:1:248:1893:25c8:g]')
).rejects.toThrow('Invalid URL')
// 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(got('http://192.168.0.1')).rejects.toThrow(
'The IP of the domain is reserved!'
)
// Commonly used for metadata services in cloud environments
await expect(got('http://169.254.169.254')).rejects.toThrow(
'The IP of the domain is reserved!'
)
// A public IPv4 address
nock('http://1.1.1.1').get('/').reply(200)
await got('http://1.1.1.1')
})
it('handles IPv6 addresses', async () => {
// This is 127.0.0.1 mapped to IPv6
await expect(got('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 got('http://[::ffff:101:101]')
// A public IPv6 address
nock('http://[2606:2800:220:1:248:1893:25c8:1946]').get('/').reply(200)
await got('http://[2606:2800:220:1:248:1893:25c8:1946]')
// A private IPv6 address
await expect(got('http://[::1]')).rejects.toThrow(
'The IP of the domain is reserved!'
)
})
it('handles hostnames with brackets in it', async () => {
await expect(got('http://[hostname1.com')).rejects.toThrow('Invalid URL')
await expect(got('http://[hostnam]e2.com')).rejects.toThrow('Invalid URL')
await expect(
got('http://[2606:2800:220:1:248:1893:25c8:g]')
).rejects.toThrow('Invalid URL')
})
})
})
{
"name": "got-ssrf",
"version": "1.3.5",
"version": "2.0.0",
"description": "Protect Got requests from SSRF",

@@ -11,3 +11,3 @@ "type": "module",

"engines": {
"node": ">=14"
"node": ">=16"
},

@@ -23,3 +23,3 @@ "scripts": {

"debug": "^4.3.2",
"got": "^12.0.0",
"got": "^13.0.0",
"ipaddr.js": "^2.0.1"

@@ -30,2 +30,3 @@ },

"@janejeon/prettier-config": "^1.1.0",
"cacheable-lookup": "^7.0.0",
"husky": "^8.0.1",

@@ -32,0 +33,0 @@ "jest": "^29.5.0",

<h1 align="center">Welcome to got-ssrf 👋</h1>
[![CircleCI](https://circleci.com/gh/hanover-computing/got-ssrf/tree/master.svg?style=shield)](https://circleci.com/gh/hanover-computing/got-ssrf/tree/master)
[![GitHub Actions](https://github.com/hanover-computing/got-ssrf/actions/workflows/ci.yml/badge.svg)](https://github.com/hanover-computing/got-ssrf/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/hanover-computing/got-ssrf/branch/master/graph/badge.svg)](https://codecov.io/gh/hanover-computing/got-ssrf)

@@ -26,3 +27,3 @@ [![Version](https://img.shields.io/npm/v/got-ssrf)](https://www.npmjs.com/package/got-ssrf)

Note that this package is ESM-only; see https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c for what to do if you're using CJS (i.e. `require()`).
> Note that this package is ESM-only; see https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c for what to do if you're using CJS (i.e. `require()`).

@@ -45,2 +46,10 @@ ```js

### Security
This library is tested against a whole host of weird edge cases (a URL is not as straightforward as it seems). To see what behaviours are expected, please see the test suite.
As this library doesn't parse the URLs itself (but rather relies on got, which relies on the node `URL` module), a good rule of thumb is that whatever you'd expect from the node `URL` module, you can expect of this library as well.
If you want to disallow "weird" URLs (and trust me, there are _many_), as people may try to 'smuggle' hostnames in them (and cause SSRF that may not be caught by the `URL` module), you'll need to do an input validation of the URL (and reject the "weird" ones) _before_ passing it into got/got-ssrf.
## Run tests

@@ -54,3 +63,3 @@

👤 **Jane Jeon <me@janejeon.dev>**
👤 **Jane Jeon <git@janejeon.com>**

@@ -70,5 +79,5 @@ - Website: janejeon.dev

Copyright © 2022 [Jane Jeon <me@janejeon.dev>](https://github.com/JaneJeon).<br />
Copyright © 2023 [Jane Jeon <git@janejeon.com>](https://github.com/JaneJeon).<br />
This project is [LGPL-3.0](https://github.com/JaneJeon/got-csrf/blob/master/LICENSE) licensed.
TL;DR: you are free to import and use this library "as-is" in your code, without needing to make your code source-available or to license it under the same license as this library; however, if you do change this library and you distribute it (directly or as part of your code consuming this library), please do contribute back any improvements for this library and this library alone.

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc