Skip to content

Commit f45838b

Browse files
authored
Merge pull request #583 from AikidoSec/fix-path-traversal
Fix path traversal ignored if in pathname
2 parents 67a0304 + 105ac9a commit f45838b

File tree

3 files changed

+75
-5
lines changed

3 files changed

+75
-5
lines changed

library/agent/Source.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const SOURCES = [
88
"xml",
99
"subdomains",
1010
"markUnsafe",
11+
"url",
1112
] as const;
1213

1314
export type Source = (typeof SOURCES)[number];

library/sources/HTTPServer.test.ts

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import * as fetchBlockedLists from "../agent/api/fetchBlockedLists";
1313
import { mkdtemp, writeFile, unlink } from "fs/promises";
1414
import { exec } from "child_process";
1515
import { promisify } from "util";
16+
import { FileSystem } from "../sinks/FileSystem";
17+
import { Path } from "../sinks/Path";
1618
const execAsync = promisify(exec);
1719

1820
// Before require("http")
@@ -48,7 +50,7 @@ const agent = createTestAgent({
4850
token: new Token("123"),
4951
api,
5052
});
51-
agent.start([new HTTPServer()]);
53+
agent.start([new HTTPServer(), new FileSystem(), new Path()]);
5254

5355
wrap(fetchBlockedLists, "fetchBlockedLists", function fetchBlockedLists() {
5456
return async function fetchBlockedLists(): Promise<{
@@ -78,6 +80,8 @@ t.beforeEach(() => {
7880

7981
const http = require("http") as typeof import("http");
8082
const https = require("https") as typeof import("https");
83+
const { readFileSync } = require("fs") as typeof import("fs");
84+
const path = require("path") as typeof import("path");
8185

8286
t.test("it wraps the createServer function of http module", async () => {
8387
const server = http.createServer((req, res) => {
@@ -550,9 +554,6 @@ t.test("it wraps on request event of http", async () => {
550554
});
551555

552556
t.test("it wraps on request event of https", async () => {
553-
const { readFileSync } = require("fs");
554-
const path = require("path");
555-
556557
// Otherwise, the self-signed certificate will be rejected
557558
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
558559

@@ -719,3 +720,71 @@ t.test(
719720
});
720721
}
721722
);
723+
724+
/**
725+
* Explanation:
726+
* - Makes a request to the server with a path traversal attack inside the pathname
727+
* - The /../ is not removed from the path during the request because path normalization is not applied (by default many http libraries do this, e.g. if new URL(...) is used)
728+
* - The server gets the raw string path from the HTTP header that is not normalized and passes it to path.join
729+
*/
730+
t.test("it blocks path traversal in path", async (t) => {
731+
const server = http.createServer((req, res) => {
732+
try {
733+
// req.url contains only the path and query string, not the full URL
734+
// e.g. "/foo/bar?baz=qux"
735+
// req.url is not sanitized, it's a raw string, thats why /../ is not removed
736+
const path = req.url || "/";
737+
const file = readFileSync(join(__dirname, path));
738+
739+
res.statusCode = 200;
740+
res.end(file);
741+
} catch (error) {
742+
res.statusCode = 500;
743+
if (error instanceof Error) {
744+
res.end(error.message);
745+
return;
746+
}
747+
res.end("Internal server error");
748+
}
749+
});
750+
751+
await new Promise<void>((resolve) => {
752+
server.listen(3327, async () => {
753+
const response = await new Promise((resolve, reject) => {
754+
// Directly using http.request with a url-like object to prevent path normalization that would remove /../
755+
const req = http.request(
756+
{
757+
hostname: "localhost",
758+
port: 3327,
759+
path: "/../package.json", // Path traversal attempt
760+
method: "GET",
761+
},
762+
(res) => {
763+
let data = "";
764+
765+
res.on("data", (chunk) => {
766+
data += chunk;
767+
});
768+
769+
res.on("end", () => {
770+
resolve(data);
771+
});
772+
}
773+
);
774+
775+
req.on("error", (err) => {
776+
reject(err);
777+
});
778+
779+
req.end();
780+
});
781+
782+
t.equal(
783+
response,
784+
"Zen has blocked a path traversal attack: path.join(...) originating from url."
785+
);
786+
server.close();
787+
resolve();
788+
});
789+
});
790+
});

library/vulnerabilities/ssrf/inspectDNSLookupCalls.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ wrap(console, "log", function log() {
1818
const context: Context = {
1919
remoteAddress: "::1",
2020
method: "POST",
21-
url: "http://localhost:4000",
21+
url: "http://app.example.com:4000",
2222
query: {},
2323
headers: {},
2424
body: {

0 commit comments

Comments
 (0)