Skip to content
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

Enable SSRF redirect protection and add breaking test for AWS SDK v3 #346

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c3fda7a
Enable SSRF redirect protection and add breaking test for AWS SDK v3
hansott Aug 28, 2024
284e8c6
Wrap response handler if there's one, don't consume body
hansott Aug 28, 2024
67726c1
Fix types
hansott Aug 28, 2024
e8f1548
Delete library/sinks/AwsSDKVersion3.ts
hansott Aug 28, 2024
bfba91e
Set timeouts
hansott Aug 28, 2024
5a1c464
Merge branch 'patch-ssrf-enable' of github.com:AikidoSec/node-RASP in…
hansott Aug 28, 2024
e2dc0a4
Don't fail other unit test jobs if one fails
hansott Aug 29, 2024
4c6d7d4
Wrap all methods to add event listeners
hansott Aug 29, 2024
a76a132
Extract function
hansott Aug 29, 2024
21609ee
Test whether redirect isn't added twice
hansott Aug 29, 2024
64cfe5f
Split in separate test (because it hangs on 19+, even without firewall)
hansott Aug 29, 2024
fb074f6
Consume body in test
hansott Aug 29, 2024
9d8ed08
Update comment
hansott Aug 29, 2024
2cd72d0
Combine imports
hansott Aug 29, 2024
db5fa22
Rewrite for readability
hansott Aug 29, 2024
8eac1b5
Add comments to wrapping
hansott Aug 30, 2024
6bde6ab
Fix additional dependencies
timokoessler Sep 2, 2024
beea69d
Add direct test with event listener
timokoessler Sep 2, 2024
1557146
Merge branch 'beta' of github.com:AikidoSec/node-RASP into patch-ssrf…
hansott Nov 20, 2024
a0c2dc0
Fixes after merging
hansott Nov 20, 2024
f8f9263
Add infinite loop test
hansott Nov 20, 2024
bae368d
Protect against infinite loops in `getRedirectOrigin`
hansott Nov 20, 2024
0201229
Add more tests for `getRedirectOrigin`
hansott Nov 20, 2024
cce03ea
Remove redundant check
hansott Nov 20, 2024
dc1c3f7
Simplify algo
hansott Nov 20, 2024
bc88f6c
Use agent helper function
hansott Nov 21, 2024
d197d60
Fix test
hansott Nov 21, 2024
51c055a
Merge branch 'main' of github.com:AikidoSec/node-RASP into patch-ssrf…
hansott Dec 18, 2024
64d57f4
Improved comment
hansott Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
ports:
- "27015:3306"
strategy:
fail-fast: false
matrix:
node-version: [16.x, 18.x, 20.x, 22.x]
steps:
Expand Down
4,066 changes: 2,833 additions & 1,233 deletions library/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,9 @@
"lint": "npm run lint-eslint && npm run lint-tsc",
"lint-eslint": "eslint '**/*.ts'",
"lint-tsc": "tsc --noEmit"
},
"dependencies": {
timokoessler marked this conversation as resolved.
Show resolved Hide resolved
"@aws-sdk/client-s3": "^3.637.0",
"@aws-sdk/credential-providers": "^3.637.0"
}
}
48 changes: 48 additions & 0 deletions library/sinks/AwsSDKVersion3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CreateBucketCommand, ListBucketsCommand } from "@aws-sdk/client-s3";
import { fromEnv } from "@aws-sdk/credential-providers";
import * as t from "tap";
import { Agent } from "../agent/Agent";
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting";
import { LoggerForTesting } from "../agent/logger/LoggerForTesting";
import { HTTPRequest } from "./HTTPRequest";

t.test("it works", async (t) => {
const logger = new LoggerForTesting();
const agent = new Agent(
true,
logger,
new ReportingAPIForTesting(),
undefined,
undefined
);

agent.start([new HTTPRequest()]);

const { S3Client } = require("@aws-sdk/client-s3");

process.env.AWS_ACCESS_KEY_ID = "test";
process.env.AWS_SECRET_ACCESS_KEY = "test";

const s3 = new S3Client({
region: "us-east-1",
endpoint: "http://localhost:9090",
credentials: fromEnv(),
forcePathStyle: true,
});

const name = "bucket";

try {
await s3.send(new CreateBucketCommand({ Bucket: name }));
} catch (err) {
if (err.Code !== "BucketAlreadyOwnedByYou") {
throw err;
}
}

const buckets = await s3.send(new ListBucketsCommand({}));
t.same(
buckets.Buckets?.map((bucket) => bucket.Name),
[name]
);
});
2 changes: 1 addition & 1 deletion library/sinks/HTTPRequest.axios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const context: Context = {
const redirectTestUrl =
"http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com";

t.test("it works", { skip: "SSRF redirect check disabled atm" }, async (t) => {
t.test("it works", async (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
Expand Down
69 changes: 69 additions & 0 deletions library/sinks/HTTPRequest.context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* 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, getContext, runWithContext } from "../agent/Context";
import { LoggerNoop } from "../agent/logger/LoggerNoop";
import { HTTPRequest } from "./HTTPRequest";

const source =
"http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com/ssrf-test";
const destination = "http://127.0.0.1/test";

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

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

const http = require("http");

runWithContext(context, () => {
const request = http.request(source, (res) => {
res.on("data", () => {});
});

request.on("response", (res) => {
t.same(
getContext().outgoingRequestRedirects.map((r) => ({
source: r.source.toString(),
destination: r.destination.toString(),
})),
[
{
source: source,
destination: destination,
},
]
);
t.end();
});

request.on("error", (e) => {
t.fail(e);
t.end();
});

request.end();
});
});
2 changes: 1 addition & 1 deletion library/sinks/HTTPRequest.followRedirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const context: Context = {
const redirectTestUrl =
"http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com";

t.test("it works", { skip: "SSRF redirect check disabled atm" }, (t) => {
t.test("it works", (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
Expand Down
125 changes: 77 additions & 48 deletions library/sinks/HTTPRequest.needle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,30 @@ 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 { getMajorNodeVersion } from "../helpers/getNodeVersion";
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";
function createContext(obj = {}): Context {
return {
...{
remoteAddress: "::1",
method: "POST",
url: "http://localhost:4000",
query: {},
headers: {},
body: {
image: "http://localhost:5000/api/internal",
},
cookies: {},
routeParams: {},
source: "express",
route: "/posts/:id",
},
...obj,
};
}

t.test("it works", { skip: "SSRF redirect check disabled atm" }, async (t) => {
t.test("it works", async (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
Expand All @@ -38,7 +41,7 @@ t.test("it works", { skip: "SSRF redirect check disabled atm" }, async (t) => {

const needle = require("needle");

await runWithContext(context, async () => {
await runWithContext(createContext(), async () => {
await needle("get", "https://www.aikido.dev");
});

Expand All @@ -49,42 +52,68 @@ t.test("it works", { skip: "SSRF redirect check disabled atm" }, async (t) => {

const error = await t.rejects(
async () =>
await runWithContext(context, async () => {
await runWithContext(createContext(), async () => {
await needle("get", "http://localhost:5000/api/internal");
})
);

t.ok(error instanceof Error);
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<void>((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();
}
);
});
}
);
});

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

t.test(
"it detects SSRF attacks with redirects",
{
skip:
getMajorNodeVersion() >= 19 ? "This request hangs on Node.js 19+" : false,
},
async (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
new ReportingAPIForTesting(),
new Token("123"),
undefined
);
agent.start([new HTTPRequest()]);

const needle = require("needle");

await runWithContext(
createContext({ body: { image: `${redirectTestUrl}/ssrf-test-domain` } }),
async () => {
await new Promise<void>((resolve) => {
needle.request(
"get",
`${redirectTestUrl}/ssrf-test-domain`,
{},
{
/* eslint-disable camelcase */
follow_max: 1,
open_timeout: 5000,
response_timeout: 5000,
read_timeout: 5000,
/* eslint-enable camelcase */
},
(error, response) => {
t.ok(error instanceof Error);
t.match(
error?.message,
/Aikido firewall has blocked a server-side request forgery/
);
resolve();
}
);
});
}
);
}
);
2 changes: 1 addition & 1 deletion library/sinks/HTTPRequest.nodeFetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const context: Context = {
const redirectTestUrl =
"http://firewallssrfredirects-env-2.eba-7ifve22q.eu-north-1.elasticbeanstalk.com";

t.test("it works", { skip: "SSRF redirect check disabled atm" }, async (t) => {
t.test("it works", async (t) => {
const agent = new Agent(
true,
new LoggerNoop(),
Expand Down
Loading
Loading