Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 library/agent/Source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const SOURCES = [
"xml",
"subdomains",
"markUnsafe",
"url",
] as const;

export type Source = (typeof SOURCES)[number];
77 changes: 73 additions & 4 deletions library/sources/HTTPServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import { mkdtemp, writeFile, unlink } from "fs/promises";
import { exec } from "child_process";
import { promisify } from "util";
import { FileSystem } from "../sinks/FileSystem";
import { Path } from "../sinks/Path";
const execAsync = promisify(exec);

// Before require("http")
Expand Down Expand Up @@ -48,7 +50,7 @@
token: new Token("123"),
api,
});
agent.start([new HTTPServer()]);
agent.start([new HTTPServer(), new FileSystem(), new Path()]);

wrap(fetchBlockedLists, "fetchBlockedLists", function fetchBlockedLists() {
return async function fetchBlockedLists(): Promise<{
Expand Down Expand Up @@ -78,6 +80,8 @@

const http = require("http") as typeof import("http");
const https = require("https") as typeof import("https");
const { readFileSync } = require("fs") as typeof import("fs");
const path = require("path") as typeof import("path");

t.test("it wraps the createServer function of http module", async () => {
const server = http.createServer((req, res) => {
Expand Down Expand Up @@ -550,9 +554,6 @@
});

t.test("it wraps on request event of https", async () => {
const { readFileSync } = require("fs");
const path = require("path");

// Otherwise, the self-signed certificate will be rejected
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

Expand Down Expand Up @@ -719,3 +720,71 @@
});
}
);

/**
* Explanation:
* - Makes a request to the server with a path traversal attack inside the pathname
* - 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)
* - The server gets the raw string path from the HTTP header that is not normalized and passes it to path.join
*/
t.test("it blocks path traversal in path", async (t) => {
const server = http.createServer((req, res) => {
try {
// req.url contains only the path and query string, not the full URL
// e.g. "/foo/bar?baz=qux"
// req.url is not sanitized, it's a raw string, thats why /../ is not removed
const path = req.url || "/";
const file = readFileSync(join(__dirname, path));

res.statusCode = 200;
res.end(file);
} catch (error) {
res.statusCode = 500;
if (error instanceof Error) {
res.end(error.message);
return;
}
res.end("Internal server error");
}
});

await new Promise<void>((resolve) => {
server.listen(3327, async () => {
const response = await new Promise((resolve, reject) => {
// Directly using http.request with a url-like object to prevent path normalization that would remove /../
const req = http.request(
{
hostname: "localhost",
port: 3327,
path: "/../package.json", // Path traversal attempt
method: "GET",
},
(res) => {
let data = "";

res.on("data", (chunk) => {
data += chunk;
});

res.on("end", () => {
resolve(data);
});
}
);

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

req.end();
});

t.equal(
response,
"Zen has blocked a path traversal attack: path.join(...) originating from url."
);
server.close();
resolve();
});
});
});
2 changes: 1 addition & 1 deletion library/vulnerabilities/ssrf/inspectDNSLookupCalls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ wrap(console, "log", function log() {
const context: Context = {
remoteAddress: "::1",
method: "POST",
url: "http://localhost:4000",
url: "http://app.example.com:4000",
query: {},
headers: {},
body: {
Expand Down