Skip to content

Commit 2dee302

Browse files
Handle malformed CommandBoard paths
1 parent 8a86d14 commit 2dee302

3 files changed

Lines changed: 71 additions & 37 deletions

File tree

apps/commandboard-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"build": "node scripts/check-assets.js && vite build",
88
"dev": "vite --host 0.0.0.0",
99
"start": "node server.js",
10+
"test": "vitest run server.test.js",
1011
"test:e2e": "playwright test"
1112
},
1213
"dependencies": {

apps/commandboard-web/server.js

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { createCommandBoardServer } from "../commandboard-api/dist/index.js";
77
const appDirectory = fileURLToPath(new URL(".", import.meta.url));
88
const distDirectory = resolve(appDirectory, "dist");
99
const indexFile = join(distDirectory, "index.html");
10-
const apiServer = createCommandBoardServer();
1110
const port = readPort(process.env.PORT, 4173);
1211

1312
const mimeTypes = {
@@ -21,50 +20,53 @@ const mimeTypes = {
2120
".webmanifest": "application/manifest+json; charset=utf-8"
2221
};
2322

24-
createServer((request, response) => {
25-
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
23+
export function createCommandBoardWebServer() {
24+
const apiServer = createCommandBoardServer();
2625

27-
if (url.pathname === "/health" || url.pathname.startsWith("/api/")) {
28-
apiServer.emit("request", request, response);
29-
return;
30-
}
26+
return createServer((request, response) => {
27+
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
3128

32-
if (request.method !== "GET" && request.method !== "HEAD") {
33-
response.writeHead(405, { allow: "GET, HEAD" });
34-
response.end("Method not allowed");
35-
return;
36-
}
29+
if (url.pathname === "/health" || url.pathname.startsWith("/api/")) {
30+
apiServer.emit("request", request, response);
31+
return;
32+
}
3733

38-
let file;
39-
try {
40-
file = resolveStaticPath(url.pathname);
41-
} catch (error) {
42-
if (error instanceof URIError) {
43-
response.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
44-
response.end("Invalid path encoding");
34+
if (request.method !== "GET" && request.method !== "HEAD") {
35+
response.writeHead(405, { allow: "GET, HEAD" });
36+
response.end("Method not allowed");
4537
return;
4638
}
47-
throw error;
48-
}
4939

50-
if (!file) {
51-
response.writeHead(403);
52-
response.end("Forbidden");
53-
return;
54-
}
40+
let file;
41+
try {
42+
file = resolveStaticPath(url.pathname);
43+
} catch (error) {
44+
if (error instanceof URIError) {
45+
response.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
46+
response.end("Invalid path encoding");
47+
return;
48+
}
49+
throw error;
50+
}
5551

56-
sendFile(file, request.method === "HEAD", response);
57-
}).listen(port, () => {
58-
console.log(`CommandBoard.run PWA listening on http://localhost:${port}`);
59-
});
52+
if (!file) {
53+
response.writeHead(403);
54+
response.end("Forbidden");
55+
return;
56+
}
6057

61-
function resolveStaticPath(pathname) {
62-
let decodedPath;
63-
try {
64-
decodedPath = decodeURIComponent(pathname);
65-
} catch {
66-
return null;
67-
}
58+
sendFile(file, request.method === "HEAD", response);
59+
});
60+
}
61+
62+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
63+
createCommandBoardWebServer().listen(port, () => {
64+
console.log(`CommandBoard.run PWA listening on http://localhost:${port}`);
65+
});
66+
}
67+
68+
export function resolveStaticPath(pathname) {
69+
const decodedPath = decodeURIComponent(pathname);
6870
const normalizedPath = normalize(decodedPath).replace(/^(\.\.[/\\])+/, "");
6971
let candidate = join(distDirectory, normalizedPath);
7072

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
2+
import { createCommandBoardWebServer } from "./server.js";
3+
4+
let server;
5+
let baseUrl;
6+
7+
beforeAll(async () => {
8+
server = createCommandBoardWebServer();
9+
await new Promise((resolve) => server.listen(0, resolve));
10+
const address = server.address();
11+
if (!address || typeof address === "string") {
12+
throw new Error("Expected web server to bind to a local port");
13+
}
14+
baseUrl = `http://127.0.0.1:${address.port}`;
15+
});
16+
17+
afterAll(async () => {
18+
await new Promise((resolve, reject) => {
19+
server.close((error) => (error ? reject(error) : resolve()));
20+
});
21+
});
22+
23+
describe("CommandBoard web server", () => {
24+
it("rejects malformed path encoding as a client error", async () => {
25+
const response = await fetch(`${baseUrl}/%E0%A4%A`);
26+
const body = await response.text();
27+
28+
expect(response.status).toBe(400);
29+
expect(body).toBe("Invalid path encoding");
30+
});
31+
});

0 commit comments

Comments
 (0)