fix(api): prevent path prefix traversal in admin static handler

This commit is contained in:
coo1estguy
2026-09-19 14:54:28 +05:30
parent 1625cdc8f8
commit d0ca566d6c
2 changed files with 30 additions and 3 deletions
+3 -2
View File
@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { resolve, extname, join } from "node:path";
import { resolve, extname, join, relative, isAbsolute } from "node:path";
import { existsSync, statSync, createReadStream } from "node:fs";
import { request as httpRequest } from "node:http";
@@ -155,7 +155,8 @@ export async function handleAdminRequest(
let filePath = resolve(dashboardDir, subPath);
// Security check: ensure filePath is inside dashboardDir
if (!filePath.startsWith(dashboardDir)) {
const rel = relative(dashboardDir, filePath);
if (rel.startsWith("..") || isAbsolute(rel)) {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden");
return true;
+27 -1
View File
@@ -1,5 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
import { handleAdminRequest } from "@/services/api/admin-static.js";
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
import { resolve } from "node:path";
function createMockReq(url: string, method = "GET") {
return {
@@ -39,6 +41,19 @@ function createMockRes() {
}
describe("Admin Static Server", () => {
const testDir = resolve(process.cwd(), "dist", "admin");
beforeAll(() => {
mkdirSync(testDir, { recursive: true });
writeFileSync(resolve(testDir, "index.html"), "<html>Admin</html>", "utf8");
});
afterAll(() => {
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
it("ignores non-admin requests", async () => {
const req = createMockReq("/v1/notify");
const res = createMockRes();
@@ -67,4 +82,15 @@ describe("Admin Static Server", () => {
expect(handled).toBe(true);
expect(res.statusCode).toBe(200);
});
it("blocks directory traversal attempts outside dashboardDir", async () => {
const req = createMockReq("/admin/assets/../../admin_secret/config.json");
const res = createMockRes();
const url = new URL("http://localhost:3000/admin/assets/../../admin_secret/config.json");
Object.defineProperty(url, "pathname", { value: "/admin/../admin_secret/config.json" });
const handled = await handleAdminRequest(req, res, url);
expect(handled).toBe(true);
expect(res.writeHead).toHaveBeenCalledWith(403, { "Content-Type": "text/plain" });
});
});