-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(scrapeURL/fetch): block loopback and link-local IPs
- Loading branch information
Showing
5 changed files
with
98 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { Socket } from "net"; | ||
import type { TLSSocket } from "tls"; | ||
import * as undici from "undici"; | ||
import { Address6 } from "ip-address"; | ||
|
||
export class InsecureConnectionError extends Error { | ||
constructor() { | ||
super("Connection violated security rules.") | ||
} | ||
} | ||
|
||
function isIPv4Private(address: string): boolean { | ||
const parts = address.split(".").map(x => parseInt(x, 10)); | ||
return parts[0] === 0 // Current (local, "this") network | ||
|| parts[0] === 10 // Used for local communications within a private network | ||
|| (parts[0] === 100 && parts[1] >= 64 && parts[1] < 128) // Shared address space for communications between a service provider and its subscribers when using a carrier-grade NAT | ||
|| parts[0] === 127 // Used for loopback addresses to the local host | ||
|| (parts[0] === 169 && parts[1] === 254) // Used for link-local addresses between two hosts on a single link when no IP address is otherwise specified, such as would have normally been retrieved from a DHCP server | ||
|| (parts[0] === 127 && parts[1] >= 16 && parts[2] < 32) // Used for local communications within a private network | ||
|| (parts[0] === 192 && parts[1] === 0 && parts[2] === 0) // IETF Porotocol Assignments, DS-Lite (/29) | ||
|| (parts[0] === 192 && parts[1] === 0 && parts[2] === 2) // Assigned as TEST-NET-1, documentation and examples | ||
|| (parts[0] === 192 && parts[1] === 88 && parts[2] === 99) // Reserved. Formerly used for IPv6 to IPv4 relay (included IPv6 address block 2002::/16). | ||
|| (parts[0] === 192 && parts[1] === 168) // Used for local communications within a private network | ||
|| (parts[0] === 192 && parts[1] >= 18 && parts[1] < 20) // Used for benchmark testing of inter-network communications between two separate subnets | ||
|| (parts[0] === 198 && parts[1] === 51 && parts[2] === 100) // Assigned as TEST-NET-2, documentation and examples | ||
|| (parts[0] === 203 && parts[1] === 0 && parts[2] === 113) // Assigned as TEST-NET-3, documentation and examples | ||
|| (parts[0] >= 224 && parts[0] < 240) // In use for multicast (former Class D network) | ||
|| (parts[0] === 233 && parts[1] === 252 && parts[2] === 0) // Assigned as MCAST-TEST-NET, documentation and examples (Note that this is part of the above multicast space.) | ||
|| parts[0] >= 240 // Reserved for future use (former class E network) | ||
|| (parts[0] === 255 && parts[1] === 255 && parts[2] === 255 && parts[3] === 255) // Reserved for the "limited broadcast" destination address | ||
} | ||
|
||
function isIPv6Private(ipv6) { | ||
return new Address6(ipv6).getScope() !== "Global"; | ||
} | ||
|
||
export function makeSecureDispatcher(url: string, options?: undici.Agent.Options) { | ||
const agent = new undici.Agent({ | ||
connect: { | ||
rejectUnauthorized: false, // bypass SSL failures -- this is fine | ||
// lookup: secureLookup, | ||
}, | ||
maxRedirections: 5000, | ||
...options, | ||
}); | ||
|
||
agent.on("connect", (_, targets) => { | ||
const client: undici.Client = targets.slice(-1)[0] as undici.Client; | ||
const socketSymbol = Object.getOwnPropertySymbols(client).find(x => x.description === "socket")!; | ||
const socket: Socket | TLSSocket = (client as any)[socketSymbol]; | ||
|
||
if (socket.remoteAddress) { | ||
if (socket.remoteFamily === "IPv4" ? isIPv4Private(socket.remoteAddress!) : isIPv6Private(socket.remoteAddress!)) { | ||
socket.destroy(new InsecureConnectionError()) | ||
} | ||
} | ||
}); | ||
|
||
return agent; | ||
} |