Skip to content

Support http2 client (SSRF) (2) #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import { Shelljs } from "../sinks/Shelljs";
import { NodeSQLite } from "../sinks/NodeSqlite";
import { BetterSQLite3 } from "../sinks/BetterSQLite3";
import { HTTP2Request } from "../sinks/HTTP2Request";

function isDebugging() {
return (
Expand Down Expand Up @@ -139,10 +140,11 @@
new Hapi(),
new NodeSQLite(),
new BetterSQLite3(),
new HTTP2Request(),
];
}

export function protect() {

Check warning on line 147 in library/agent/protect.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

exported declaration 'protect' not used within other modules
const agent = getAgent({
serverless: undefined,
});
Expand Down
67 changes: 67 additions & 0 deletions library/helpers/http2Request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { IncomingHttpHeaders } from "http2";

let _client: any;

/**
* HTTP2 Client, use only for testing
*/
export function http2Request(

Check failure on line 8 in library/helpers/http2Request.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Function 'http2Request' has too many lines (53). Maximum allowed is 50
url: URL | string,
method: string,
headers: Record<string, string>,
connectOptions?: Record<string, unknown>,
reuseClient?: boolean,
body?: string
) {
const { connect } = require("http2");
return new Promise<{ headers: IncomingHttpHeaders; body: string }>(
(resolve, reject) => {
if (!reuseClient || !_client) {
if (connectOptions) {
_client = connect(url, connectOptions);
} else {
_client = connect(url);
}
}
if (typeof url === "string") {
url = new URL(url);
}
_client.on("error", (err: Error) => {
reject(err);
});

const path = url.pathname + (url.search ? url.search : "");

const req = _client.request({
":path": path || "/",
":method": method,
"content-length": body ? Buffer.byteLength(body) : 0,
...headers,
});

let respHeaders: IncomingHttpHeaders;
let resData = "";

req.on("error", (err: Error) => {
reject(err);
});

req.on("response", (headers: Record<string, string>) => {
respHeaders = headers;
});

req.on("data", (chunk: any) => {
resData += chunk;
});

req.on("end", () => {
_client!.close();
resolve({ headers: respHeaders, body: resData });
});
if (body) {
return req.end(body);
}
req.end();
}
);
}
15 changes: 6 additions & 9 deletions library/sinks/Fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const context: Context = {
route: "/posts/:id",
};

const redirectTestUrl =
const redirectTestUrl = "http://ssrf-redirects.testssandbox.com";
const redirecTestUrl2 =
"http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com";

const redirectUrl = {
Expand Down Expand Up @@ -255,16 +256,13 @@ t.test(
...context,
...{
body: {
image:
"http://ec2-13-60-120-68.eu-north-1.compute.amazonaws.com/ssrf-test-absolute-domain",
image: `${redirecTestUrl2}/ssrf-test-absolute-domain`,
},
},
},
async () => {
const error = await t.rejects(() =>
fetch(
"http://ec2-13-60-120-68.eu-north-1.compute.amazonaws.com/ssrf-test-absolute-domain"
)
fetch(`${redirecTestUrl2}/ssrf-test-absolute-domain`)
);
if (error instanceof Error) {
t.same(
Expand Down Expand Up @@ -331,14 +329,13 @@ t.test(
...context,
...{
body: {
image:
"http://ec2-13-60-120-68.eu-north-1.compute.amazonaws.com/ssrf-test-absolute-domain",
image: `${redirecTestUrl2}/ssrf-test-absolute-domain`,
},
},
},
async () => {
const response = await fetch(
"http://ec2-13-60-120-68.eu-north-1.compute.amazonaws.com/ssrf-test-absolute-domain",
`${redirecTestUrl2}/ssrf-test-absolute-domain`,
{
redirect: "manual",
}
Expand Down
96 changes: 96 additions & 0 deletions library/sinks/HTTP2Request.redirect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable prefer-rest-params */
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 { HTTP2Request } from "./HTTP2Request";
import { http2Request } from "../helpers/http2Request";

const context: Context = {
remoteAddress: "::1",
method: "POST",
url: "http://localhost:4000",
query: {},
headers: {},
body: {
image: "http://localhost:4000/api/internal",
},
cookies: {},
routeParams: {},
source: "express",
route: "/posts/:id",
};

const redirectTestUrl = "https://ssrf-redirects.testssandbox.com";

const redirectUrl = {
ip: `${redirectTestUrl}/ssrf-test`, // Redirects to http://127.0.0.1/test
domain: `${redirectTestUrl}/ssrf-test-domain`, // Redirects to http://local.aikido.io/test
ipTwice: `${redirectTestUrl}/ssrf-test-twice`, // Redirects to /ssrf-test
domainTwice: `${redirectTestUrl}/ssrf-test-domain-twice`, // Redirects to /ssrf-test-domain
};

t.test("it works", async (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
new ReportingAPIForTesting(),
new Token("123"),
undefined
);
agent.start([new HTTP2Request()]);

await runWithContext(
{
...context,
...{ body: { image: redirectUrl.ip } },
},
async () => {
const { headers } = await http2Request(redirectUrl.ip, "GET", {});
t.same(headers[":status"], 302);
t.same(headers.location, "http://127.0.0.1/test");

try {
const { headers } = await http2Request(
"http://127.0.0.1/test",
"GET",
{}
);
t.fail();
} catch (error) {
t.match(
error.message,
/Aikido firewall has blocked a server-side request forgery: http2.request.* originating from body.image/
);
}
}
);

await runWithContext(
{
...context,
...{ body: { image: redirectUrl.ip } },
},
async () => {
const { headers } = await http2Request(redirectUrl.domain, "GET", {});
t.same(headers[":status"], 302);
t.same(headers.location, "http://local.aikido.io/test");

try {
const { headers } = await http2Request(
"http://local.aikido.io/test",
"GET",
{}
);
t.fail();
} catch (error) {
t.match(
error.message,
/Aikido firewall has blocked a server-side request forgery: http2.request.* originating from body.image/
);
}
}
);
});
Loading
Loading