diff --git a/src/app/api/v1/[...path]/route.ts b/src/app/api/v1/[...path]/route.ts index 7955311..73390e4 100644 --- a/src/app/api/v1/[...path]/route.ts +++ b/src/app/api/v1/[...path]/route.ts @@ -4,6 +4,9 @@ import { readToken } from "../../../../server/session"; async function proxy(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { const { path } = await ctx.params; + if (path.some((seg) => seg === ".." || seg === "." || seg === "" || seg.includes("/") || seg.includes("\\"))) { + return new NextResponse("bad path", { status: 400 }); + } const token = readToken(req); const search = req.nextUrl.search; const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.text(); diff --git a/src/app/api/v1/proxy.route.test.ts b/src/app/api/v1/proxy.route.test.ts new file mode 100644 index 0000000..37adb51 --- /dev/null +++ b/src/app/api/v1/proxy.route.test.ts @@ -0,0 +1,22 @@ +// @vitest-environment node +import { describe, it, expect, vi } from "vitest"; +vi.mock("../../../server/backend", () => ({ backendFetch: vi.fn(async () => new Response("{}", { status: 200 })) })); +import { NextRequest } from "next/server"; +import { backendFetch } from "../../../server/backend"; +import { GET } from "./[...path]/route"; + +describe("v1 proxy route", () => { + it("rejects a path-traversal segment with 400 and never calls backendFetch", async () => { + const req = new NextRequest("http://x/api/v1/../auth/login"); + const res = await GET(req, { params: Promise.resolve({ path: ["..", "auth", "login"] }) }); + expect(res.status).toBe(400); + expect(backendFetch).not.toHaveBeenCalled(); + }); + + it("passes through a normal path", async () => { + const req = new NextRequest("http://x/api/v1/accounts"); + const res = await GET(req, { params: Promise.resolve({ path: ["accounts"] }) }); + expect(res.status).toBe(200); + expect(backendFetch).toHaveBeenCalled(); + }); +});