Skip to content

Commit 4879c29

Browse files
committed
Improve SSRF performance: re-use extracted hostname from URL
1 parent a69e463 commit 4879c29

17 files changed

+216
-75
lines changed

benchmarks/operations/benchmark.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const modules = [
3030
module: "http-request",
3131
name: "Outgoing HTTP request (`http.request`)",
3232
},
33+
{
34+
module: "undici",
35+
name: "Outgoing HTTP request (`undici.request`)",
36+
},
3337
];
3438

3539
(async () => {

benchmarks/operations/http-request.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,8 @@ const http = require("http");
33
module.exports = {
44
step: async function step() {
55
return new Promise((resolve, reject) => {
6-
const options = {
7-
hostname: "localhost",
8-
port: 10411,
9-
};
10-
11-
const req = http.request(options, (res) => {
12-
res.on("data", () => {});
6+
const req = http.request("http://localhost:10411", (res) => {
7+
res.resume();
138
res.on("end", resolve);
149
});
1510

benchmarks/operations/package-lock.json

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmarks/operations/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dependencies": {
88
"@aikidosec/firewall": "file:../../build",
99
"better-sqlite3": "^11.7.2",
10-
"mongodb": "^6.9.0"
10+
"mongodb": "^6.9.0",
11+
"undici": "^7.3.0"
1112
}
1213
}

benchmarks/operations/undici.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const undici = require("undici");
2+
3+
module.exports = {
4+
step: async function step() {
5+
return await undici.request("http://localhost:10411");
6+
},
7+
};

library/sinks/Fetch.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Wrapper } from "../agent/Wrapper";
88
import { getPortFromURL } from "../helpers/getPortFromURL";
99
import { tryParseURL } from "../helpers/tryParseURL";
1010
import { checkContextForSSRF } from "../vulnerabilities/ssrf/checkContextForSSRF";
11+
import { Hostname } from "../vulnerabilities/ssrf/Hostname";
1112
import { inspectDNSLookupCalls } from "../vulnerabilities/ssrf/inspectDNSLookupCalls";
1213
import { wrapDispatch } from "./undici/wrapDispatch";
1314

@@ -16,13 +17,13 @@ export class Fetch implements Wrapper {
1617

1718
private inspectHostname(
1819
agent: Agent,
19-
hostname: string,
20+
hostname: URL,
2021
port: number | undefined
2122
): InterceptorResult {
2223
// Let the agent know that we are connecting to this hostname
2324
// This is to build a list of all hostnames that the application is connecting to
2425
if (typeof port === "number" && port > 0) {
25-
agent.onConnectHostname(hostname, port);
26+
agent.onConnectHostname(hostname.hostname, port);
2627
}
2728
const context = getContext();
2829

@@ -31,7 +32,7 @@ export class Fetch implements Wrapper {
3132
}
3233

3334
return checkContextForSSRF({
34-
hostname: hostname,
35+
hostname: Hostname.fromURL(hostname),
3536
operation: "fetch",
3637
context: context,
3738
port: port,
@@ -44,11 +45,7 @@ export class Fetch implements Wrapper {
4445
if (typeof args[0] === "string" && args[0].length > 0) {
4546
const url = tryParseURL(args[0]);
4647
if (url) {
47-
const attack = this.inspectHostname(
48-
agent,
49-
url.hostname,
50-
getPortFromURL(url)
51-
);
48+
const attack = this.inspectHostname(agent, url, getPortFromURL(url));
5249
if (attack) {
5350
return attack;
5451
}
@@ -62,11 +59,7 @@ export class Fetch implements Wrapper {
6259
if (Array.isArray(args[0])) {
6360
const url = tryParseURL(args[0].toString());
6461
if (url) {
65-
const attack = this.inspectHostname(
66-
agent,
67-
url.hostname,
68-
getPortFromURL(url)
69-
);
62+
const attack = this.inspectHostname(agent, url, getPortFromURL(url));
7063
if (attack) {
7164
return attack;
7265
}
@@ -77,7 +70,7 @@ export class Fetch implements Wrapper {
7770
if (args[0] instanceof URL && args[0].hostname.length > 0) {
7871
const attack = this.inspectHostname(
7972
agent,
80-
args[0].hostname,
73+
args[0],
8174
getPortFromURL(args[0])
8275
);
8376
if (attack) {
@@ -89,11 +82,7 @@ export class Fetch implements Wrapper {
8982
if (args[0] instanceof Request) {
9083
const url = tryParseURL(args[0].url);
9184
if (url) {
92-
const attack = this.inspectHostname(
93-
agent,
94-
url.hostname,
95-
getPortFromURL(url)
96-
);
85+
const attack = this.inspectHostname(agent, url, getPortFromURL(url));
9786
if (attack) {
9887
return attack;
9988
}

library/sinks/HTTPRequest.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { InterceptorResult } from "../agent/hooks/InterceptorResult";
77
import { Wrapper } from "../agent/Wrapper";
88
import { getPortFromURL } from "../helpers/getPortFromURL";
99
import { checkContextForSSRF } from "../vulnerabilities/ssrf/checkContextForSSRF";
10+
import { Hostname } from "../vulnerabilities/ssrf/Hostname";
1011
import { inspectDNSLookupCalls } from "../vulnerabilities/ssrf/inspectDNSLookupCalls";
1112
import { isRedirectToPrivateIP } from "../vulnerabilities/ssrf/isRedirectToPrivateIP";
1213
import { getUrlFromHTTPRequestArgs } from "./http-request/getUrlFromHTTPRequestArgs";
@@ -34,7 +35,7 @@ export class HTTPRequest implements Wrapper {
3435

3536
// Check if the hostname is inside the context
3637
const foundDirectSSRF = checkContextForSSRF({
37-
hostname: url.hostname,
38+
hostname: Hostname.fromURL(url),
3839
operation: `${module}.request`,
3940
context: context,
4041
port: port,

library/sinks/Undici.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { InterceptorResult } from "../agent/hooks/InterceptorResult";
77
import { Wrapper } from "../agent/Wrapper";
88
import { getSemverNodeVersion } from "../helpers/getNodeVersion";
99
import { isVersionGreaterOrEqual } from "../helpers/isVersionGreaterOrEqual";
10+
import { tryParseURL } from "../helpers/tryParseURL";
1011
import { checkContextForSSRF } from "../vulnerabilities/ssrf/checkContextForSSRF";
12+
import { Hostname } from "../vulnerabilities/ssrf/Hostname";
1113
import { inspectDNSLookupCalls } from "../vulnerabilities/ssrf/inspectDNSLookupCalls";
1214
import { wrapDispatch } from "./undici/wrapDispatch";
1315
import { wrapExport } from "../agent/hooks/wrapExport";
@@ -40,8 +42,13 @@ export class Undici implements Wrapper {
4042
return undefined;
4143
}
4244

45+
const hostnameURL = tryParseURL(`http://${hostname}`);
46+
if (!hostnameURL) {
47+
return undefined;
48+
}
49+
4350
return checkContextForSSRF({
44-
hostname: hostname,
51+
hostname: Hostname.fromURL(hostnameURL),
4552
operation: `undici.${method}`,
4653
context,
4754
port,

library/sinks/http-request/wrapResponseHandler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isRedirectStatusCode } from "../../helpers/isRedirectStatusCode";
66
import { tryParseURL } from "../../helpers/tryParseURL";
77
import { findHostnameInContext } from "../../vulnerabilities/ssrf/findHostnameInContext";
88
import { getRedirectOrigin } from "../../vulnerabilities/ssrf/getRedirectOrigin";
9+
import { Hostname } from "../../vulnerabilities/ssrf/Hostname";
910
import { getUrlFromHTTPRequestArgs } from "./getUrlFromHTTPRequestArgs";
1011

1112
/**
@@ -78,7 +79,11 @@ function addRedirectToContext(source: URL, destination: URL, context: Context) {
7879
const sourcePort = getPortFromURL(source);
7980

8081
// Check if the source hostname is in the context - is true if it's the first redirect in the chain and the user input is the source
81-
const found = findHostnameInContext(source.hostname, context, sourcePort);
82+
const found = findHostnameInContext(
83+
Hostname.fromURL(source),
84+
context,
85+
sourcePort
86+
);
8287

8388
// If the source hostname is not in the context, check if it's a redirect in a already existing chain
8489
if (!found && context.outgoingRequestRedirects) {

library/sinks/undici/onRedirect.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Context, updateContext } from "../../agent/Context";
22
import { findHostnameInContext } from "../../vulnerabilities/ssrf/findHostnameInContext";
33
import { getRedirectOrigin } from "../../vulnerabilities/ssrf/getRedirectOrigin";
4+
import { Hostname } from "../../vulnerabilities/ssrf/Hostname";
45
import { RequestContextStorage } from "./RequestContextStorage";
56

67
/**
@@ -20,7 +21,7 @@ export function onRedirect(
2021

2122
// Check if the source hostname is in the context - is true if it's the first redirect in the chain and the user input is the source
2223
const found = findHostnameInContext(
23-
requestContext.url.hostname,
24+
Hostname.fromURL(requestContext.url),
2425
context,
2526
requestContext.port
2627
);

0 commit comments

Comments
 (0)