Skip to content

Commit c405c0a

Browse files
committed
Fix dashboard static path traversal containment
1 parent de5b06b commit c405c0a

2 files changed

Lines changed: 23 additions & 4 deletions

File tree

packages/apm-contributor-dashboard/.apm/extensions/issue-monitor/server-handler.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// const server = createServer(handler);
88

99
import { readFileSync } from "node:fs";
10-
import { join, resolve, normalize } from "node:path";
10+
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
1111
import { randomBytes } from "node:crypto";
1212
import { parsePanelReview, extractFollowUpItems } from "./logic.mjs";
1313

@@ -456,8 +456,10 @@ export function createHandler(deps) {
456456
res.end("Not found");
457457
}
458458
} else if (urlPath.startsWith("/assets/")) {
459-
const resolved = resolve(distDir, normalize(urlPath.slice(1)));
460-
if (!resolved.startsWith(resolve(distDir))) {
459+
const root = resolve(distDir);
460+
const resolved = resolve(root, normalize(urlPath.slice(1)));
461+
const rel = relative(root, resolved);
462+
if (rel.startsWith("..") || isAbsolute(rel)) {
461463
res.writeHead(403); res.end("Forbidden"); return;
462464
}
463465
serveStatic(res, resolved);

packages/apm-contributor-dashboard/tests/server.test.mjs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { describe, it, before, after, beforeEach } from "node:test";
66
import assert from "node:assert/strict";
77
import { createServer } from "node:http";
88
import { createHandler } from "../.apm/extensions/issue-monitor/server-handler.mjs";
9-
import { join, dirname } from "node:path";
9+
import { basename, join, dirname } from "node:path";
1010
import { fileURLToPath } from "node:url";
1111

1212
const __dir = dirname(fileURLToPath(import.meta.url));
@@ -617,4 +617,21 @@ describe("Path traversal protection", () => {
617617
const res = await fetch(`${baseUrl}/assets/nonexistent.js`);
618618
assert.equal(res.status, 404);
619619
});
620+
621+
it("blocks sibling prefix traversal to dist-evil paths", async () => {
622+
const evilPath = `/assets/../../${basename(DIST_DIR)}-evil/secret.txt`;
623+
const url = new URL(`${baseUrl}${evilPath}`);
624+
const http = await import("node:http");
625+
const res = await new Promise((resolve) => {
626+
const req = http.request({
627+
hostname: url.hostname,
628+
port: url.port,
629+
path: evilPath,
630+
method: "GET",
631+
}, resolve);
632+
req.end();
633+
});
634+
635+
assert.equal(res.statusCode, 403);
636+
});
620637
});

0 commit comments

Comments
 (0)