diff --git a/library/package-lock.json b/library/package-lock.json index 39d4cf24a..d3b285f75 100644 --- a/library/package-lock.json +++ b/library/package-lock.json @@ -43,6 +43,7 @@ "mongodb": "^6.3.0", "mysql": "^2.18.1", "mysql2": "^3.10.0", + "needle": "^3.3.1", "node-fetch": "^2", "percentile": "^1.6.0", "pg": "^8.11.3", @@ -8096,6 +8097,43 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/needle/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", diff --git a/library/package.json b/library/package.json index 3dd250e54..0688621a6 100644 --- a/library/package.json +++ b/library/package.json @@ -66,6 +66,7 @@ "mongodb": "^6.3.0", "mysql": "^2.18.1", "mysql2": "^3.10.0", + "needle": "^3.3.1", "node-fetch": "^2", "percentile": "^1.6.0", "pg": "^8.11.3", diff --git a/library/sinks/HTTPRequest.needle.test.ts b/library/sinks/HTTPRequest.needle.test.ts new file mode 100644 index 000000000..d94df4167 --- /dev/null +++ b/library/sinks/HTTPRequest.needle.test.ts @@ -0,0 +1,90 @@ +import * as t from "tap"; +import { Agent } from "../agent/Agent"; +import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting"; +import { Token } from "../agent/api/Token"; +import { Context, runWithContext } from "../agent/Context"; +import { LoggerNoop } from "../agent/logger/LoggerNoop"; +import { HTTPRequest } from "./HTTPRequest"; + +const context: Context = { + remoteAddress: "::1", + method: "POST", + url: "http://localhost:4000", + query: {}, + headers: {}, + body: { + image: "http://localhost:5000/api/internal", + }, + cookies: {}, + routeParams: {}, + source: "express", + route: "/posts/:id", +}; + +const redirectTestUrl = + "http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com"; + +t.test("it works", async (t) => { + const agent = new Agent( + true, + new LoggerNoop(), + new ReportingAPIForTesting(), + new Token("123"), + undefined + ); + agent.start([new HTTPRequest()]); + + t.same(agent.getHostnames().asArray(), []); + + const needle = require("needle"); + + await runWithContext(context, async () => { + await needle("get", "https://www.aikido.dev"); + }); + + t.same(agent.getHostnames().asArray(), [ + { hostname: "www.aikido.dev", port: 443 }, + ]); + agent.getHostnames().clear(); + + const error = await t.rejects( + async () => + await runWithContext(context, async () => { + await needle("get", "http://localhost:5000/api/internal"); + }) + ); + if (error instanceof Error) { + t.same( + error.message, + "Aikido firewall has blocked a server-side request forgery: http.request(...) originating from body.image" + ); + } + + await runWithContext( + { + ...context, + ...{ body: { image: `${redirectTestUrl}/ssrf-test-domain` } }, + }, + async () => { + await new Promise((resolve) => { + needle.request( + "get", + `${redirectTestUrl}/ssrf-test-domain`, + {}, + { + // eslint-disable-next-line camelcase + follow_max: 1, + }, + (error, response) => { + t.ok(error instanceof Error); + t.match( + error?.message, + /Aikido firewall has blocked a server-side request forgery/ + ); + resolve(); + } + ); + }); + } + ); +}); diff --git a/library/sinks/http-request/getUrlFromHTTPRequestArgs.test.ts b/library/sinks/http-request/getUrlFromHTTPRequestArgs.test.ts index f937766f1..65fdc98fe 100644 --- a/library/sinks/http-request/getUrlFromHTTPRequestArgs.test.ts +++ b/library/sinks/http-request/getUrlFromHTTPRequestArgs.test.ts @@ -100,3 +100,28 @@ t.test("Do not get port 0 from request options", async (t) => { new URL("https://localhost") ); }); + +t.test("Pass port as string", async (t) => { + t.same( + getURL( + [{ protocol: "https:", hostname: "localhost", port: "4000" }], + "https" + ), + new URL("https://localhost:4000") + ); + t.same( + getURL(["https://localhost", { port: "4000" }], "https"), + new URL("https://localhost:4000") + ); +}); + +t.test("Pass host instead of hostname", async (t) => { + t.same( + getURL([{ protocol: "https:", host: "localhost:4000" }], "https"), + new URL("https://localhost:4000") + ); + t.same( + getURL(["https://localhost", { host: "test.dev" }], "https"), + new URL("https://test.dev") + ); +}); diff --git a/library/sinks/http-request/getUrlFromHTTPRequestArgs.ts b/library/sinks/http-request/getUrlFromHTTPRequestArgs.ts index 90fdf0d32..2a5969847 100644 --- a/library/sinks/http-request/getUrlFromHTTPRequestArgs.ts +++ b/library/sinks/http-request/getUrlFromHTTPRequestArgs.ts @@ -71,10 +71,18 @@ function getUrlFromRequestOptions( str += "//"; if (typeof options.hostname === "string") { str += options.hostname; + } else if (typeof options.host === "string") { + str += options.host; } - if (typeof options.port === "number" && options.port > 0) { - str += `:${options.port}`; + if (options.port) { + if (typeof options.port === "number" && options.port > 0) { + str += `:${options.port}`; + } + if (typeof options.port === "string" && options.port.length > 0) { + str += `:${options.port}`; + } } + if (typeof options.path === "string") { str += options.path; } @@ -98,6 +106,8 @@ function mergeURLWithRequestOptions( urlStr += "//"; if (options.hostname) { urlStr += options.hostname; + } else if (options.host) { + urlStr += options.host; } else { urlStr += url.hostname; }