Skip to content

Commit 4837994

Browse files
committed
Merge branch 'main' of github.com:AikidoSec/node-RASP into beta
* 'main' of github.com:AikidoSec/node-RASP: Update comment Update comment Update comment Update comment Additional main branch test fixes Fix unit tests Fix origin and referer header check, add tests Ignore origin and referer headers as well add blogpost Extract function for localhost host header check Skip tests on node 16 Skip undici test for node 16 Switch lines for readability Add tests and improve check Fix false positive for application doing a request to itself on localhost
2 parents 7a8164d + 59bb172 commit 4837994

9 files changed

+653
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ It protects your Node.js apps by preventing user input containing dangerous stri
2020
Zen will autonomously protect your Node.js applications against:
2121

2222
* 🛡️ [NoSQL injection attacks](https://www.aikido.dev/blog/web-application-security-vulnerabilities)
23-
* 🛡️ [SQL injection attacks]([https://www.aikido.dev/blog/web-application-security-vulnerabilities](https://owasp.org/www-community/attacks/SQL_Injection))
23+
* 🛡️ [SQL injection attacks](https://www.aikido.dev/blog/the-state-of-sql-injections)
2424
* 🛡️ [Command injection attacks](https://owasp.org/www-community/attacks/Command_Injection)
2525
* 🛡️ [Prototype pollution](./docs/prototype-pollution.md)
2626
* 🛡️ [Path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal)

library/sinks/Fetch.localhost.test.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* eslint-disable prefer-rest-params */
2+
import * as t from "tap";
3+
import { Agent } from "../agent/Agent";
4+
import { createServer, Server } from "http";
5+
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting";
6+
import { Token } from "../agent/api/Token";
7+
import { Context, runWithContext } from "../agent/Context";
8+
import { LoggerNoop } from "../agent/logger/LoggerNoop";
9+
import { Fetch } from "./Fetch";
10+
11+
function createContext({
12+
url,
13+
hostHeader,
14+
body,
15+
additionalHeaders = {},
16+
}: {
17+
url: string;
18+
hostHeader: string;
19+
body: unknown;
20+
additionalHeaders?: Record<string, string>;
21+
}): Context {
22+
return {
23+
url: url,
24+
method: "GET",
25+
headers: {
26+
host: hostHeader,
27+
connection: "keep-alive",
28+
"cache-control": "max-age=0",
29+
"sec-ch-ua":
30+
'"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
31+
"sec-ch-ua-mobile": "?0",
32+
"sec-ch-ua-platform": '"macOS"',
33+
"upgrade-insecure-requests": "1",
34+
"user-agent":
35+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
36+
accept:
37+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
38+
"sec-fetch-site": "none",
39+
"sec-fetch-mode": "navigate",
40+
"sec-fetch-user": "?1",
41+
"sec-fetch-dest": "document",
42+
"accept-encoding": "gzip, deflate, br, zstd",
43+
"accept-language": "nl,en;q=0.9,en-US;q=0.8",
44+
...additionalHeaders,
45+
},
46+
route: "/",
47+
query: {},
48+
source: "express",
49+
routeParams: {},
50+
cookies: {},
51+
remoteAddress: "127.0.0.1",
52+
subdomains: [],
53+
body: body,
54+
};
55+
}
56+
57+
const agent = new Agent(
58+
true,
59+
new LoggerNoop(),
60+
new ReportingAPIForTesting(),
61+
new Token("123"),
62+
undefined
63+
);
64+
65+
agent.start([new Fetch()]);
66+
67+
const port = 1341;
68+
const serverUrl = `http://localhost:${port}`;
69+
const hostHeader = `localhost:${port}`;
70+
71+
let server: Server;
72+
t.before(async () => {
73+
server = createServer((_, res) => {
74+
res.writeHead(200, { "Content-Type": "text/plain" });
75+
res.end("Hello World\n");
76+
});
77+
78+
return new Promise<void>((resolve) => {
79+
server.listen(port, resolve);
80+
server.unref();
81+
});
82+
});
83+
84+
t.test(
85+
"it does not block request to localhost with same port",
86+
{ skip: !global.fetch ? "fetch is not available" : false },
87+
async (t) => {
88+
await runWithContext(
89+
createContext({
90+
url: serverUrl,
91+
hostHeader: hostHeader,
92+
body: {},
93+
}),
94+
async () => {
95+
// Server doing a request to itself
96+
const response = await fetch(`${serverUrl}/favicon.ico`);
97+
// The server should respond with a 200
98+
t.same(response.status, 200);
99+
}
100+
);
101+
}
102+
);
103+
104+
t.test(
105+
"it does not block request to localhost with same port using the origin header",
106+
{ skip: !global.fetch ? "fetch is not available" : false },
107+
async (t) => {
108+
await runWithContext(
109+
createContext({
110+
url: serverUrl,
111+
hostHeader: "",
112+
body: {},
113+
additionalHeaders: {
114+
origin: serverUrl,
115+
},
116+
}),
117+
async () => {
118+
// Server doing a request to itself
119+
const response = await fetch(`${serverUrl}/favicon.ico`);
120+
// The server should respond with a 200
121+
t.same(response.status, 200);
122+
}
123+
);
124+
}
125+
);
126+
127+
t.test(
128+
"it does not block request to localhost with same port using the referer header",
129+
{ skip: !global.fetch ? "fetch is not available" : false },
130+
async (t) => {
131+
await runWithContext(
132+
createContext({
133+
url: serverUrl,
134+
hostHeader: "",
135+
body: {},
136+
additionalHeaders: {
137+
referer: serverUrl,
138+
},
139+
}),
140+
async () => {
141+
// Server doing a request to itself
142+
const response = await fetch(`${serverUrl}/favicon.ico`);
143+
// The server should respond with a 200
144+
t.same(response.status, 200);
145+
}
146+
);
147+
}
148+
);
149+
150+
t.test(
151+
"it blocks requests to other ports",
152+
{ skip: !global.fetch ? "fetch is not available" : false },
153+
async (t) => {
154+
const error = await t.rejects(async () => {
155+
await runWithContext(
156+
createContext({
157+
url: `http://localhost:${port + 1}`,
158+
hostHeader: `localhost:${port + 1}`,
159+
body: {
160+
url: `${serverUrl}/favicon.ico`,
161+
},
162+
}),
163+
async () => {
164+
// Server doing a request to localhost but with a different port
165+
// This should be blocked
166+
await fetch(`${serverUrl}/favicon.ico`);
167+
// This should not be called
168+
t.fail();
169+
}
170+
);
171+
});
172+
173+
t.ok(error instanceof Error);
174+
if (error instanceof Error) {
175+
t.same(
176+
error.message,
177+
"Zen has blocked a server-side request forgery: fetch(...) originating from body.url"
178+
);
179+
}
180+
}
181+
);
182+
183+
t.after(() => {
184+
server.close();
185+
});
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/* eslint-disable prefer-rest-params */
2+
import * as t from "tap";
3+
import { Agent } from "../agent/Agent";
4+
import { createServer, IncomingMessage, Server } from "http";
5+
import { ReportingAPIForTesting } from "../agent/api/ReportingAPIForTesting";
6+
import { Token } from "../agent/api/Token";
7+
import { Context, runWithContext } from "../agent/Context";
8+
import { LoggerNoop } from "../agent/logger/LoggerNoop";
9+
import { HTTPRequest } from "./HTTPRequest";
10+
11+
function createContext({
12+
url,
13+
hostHeader,
14+
body,
15+
}: {
16+
url: string;
17+
hostHeader: string;
18+
body: unknown;
19+
}): Context {
20+
return {
21+
url: url,
22+
method: "GET",
23+
headers: {
24+
host: hostHeader,
25+
connection: "keep-alive",
26+
"cache-control": "max-age=0",
27+
"sec-ch-ua":
28+
'"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
29+
"sec-ch-ua-mobile": "?0",
30+
"sec-ch-ua-platform": '"macOS"',
31+
"upgrade-insecure-requests": "1",
32+
"user-agent":
33+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
34+
accept:
35+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
36+
"sec-fetch-site": "none",
37+
"sec-fetch-mode": "navigate",
38+
"sec-fetch-user": "?1",
39+
"sec-fetch-dest": "document",
40+
"accept-encoding": "gzip, deflate, br, zstd",
41+
"accept-language": "nl,en;q=0.9,en-US;q=0.8",
42+
},
43+
route: "/",
44+
query: {},
45+
source: "express",
46+
routeParams: {},
47+
cookies: {},
48+
remoteAddress: "127.0.0.1",
49+
subdomains: [],
50+
body: body,
51+
};
52+
}
53+
54+
const agent = new Agent(
55+
true,
56+
new LoggerNoop(),
57+
new ReportingAPIForTesting(),
58+
new Token("123"),
59+
undefined
60+
);
61+
62+
agent.start([new HTTPRequest()]);
63+
64+
const port = 1343;
65+
const serverUrl = `http://localhost:${port}`;
66+
const hostHeader = `localhost:${port}`;
67+
68+
let server: Server;
69+
t.before(async () => {
70+
server = createServer((_, res) => {
71+
res.writeHead(200, { "Content-Type": "text/plain" });
72+
res.end("Hello World\n");
73+
});
74+
75+
return new Promise<void>((resolve) => {
76+
server.listen(port, resolve);
77+
server.unref();
78+
});
79+
});
80+
81+
t.test("it does not block request to localhost with same port", (t) => {
82+
const http = require("http");
83+
84+
runWithContext(
85+
createContext({
86+
url: serverUrl,
87+
hostHeader: hostHeader,
88+
body: {},
89+
}),
90+
() => {
91+
// Server doing a request to itself
92+
// Let's simulate a request to a favicon
93+
const request = http.request(`${serverUrl}/favicon.ico`);
94+
request.on("response", (response: IncomingMessage) => {
95+
// The server should respond with a 200
96+
// Because we'll allow requests to localhost if it's the same port
97+
t.same(response.statusCode, 200);
98+
response.on("data", () => {});
99+
response.on("end", () => {});
100+
});
101+
request.end();
102+
}
103+
);
104+
105+
const errors: Error[] = [];
106+
process.on("uncaughtException", (error) => {
107+
errors.push(error);
108+
});
109+
110+
setTimeout(() => {
111+
t.same(errors, []);
112+
t.end();
113+
}, 1000);
114+
});
115+
116+
t.test("it blocks requests to other ports", (t) => {
117+
const http = require("http");
118+
119+
runWithContext(
120+
createContext({
121+
url: `http://localhost:${port + 1}`,
122+
hostHeader: `localhost:${port + 1}`,
123+
body: {
124+
url: `${serverUrl}/favicon.ico`,
125+
},
126+
}),
127+
() => {
128+
try {
129+
// Server doing a request to localhost but with a different port
130+
// This should be blocked
131+
const request = http.request(`${serverUrl}/favicon.ico`);
132+
request.on("response", (response: IncomingMessage) => {
133+
// This should not be called
134+
t.fail();
135+
response.on("data", () => {});
136+
response.on("end", () => {});
137+
});
138+
request.end();
139+
} catch (error) {
140+
t.ok(error instanceof Error);
141+
if (error instanceof Error) {
142+
t.same(
143+
error.message,
144+
"Zen has blocked a server-side request forgery: http.request(...) originating from body.url"
145+
);
146+
}
147+
}
148+
}
149+
);
150+
151+
setTimeout(() => {
152+
t.end();
153+
}, 1000);
154+
});
155+
156+
t.after(() => {
157+
server.close();
158+
});

0 commit comments

Comments
 (0)