fix(ui): encode ids in raw material, stock and production batch API paths
Security review finding (client-side path traversal): ids taken from the URL were interpolated into API paths unencoded, so a crafted link like /raw-materials/..%2Fusers made the page call another endpoint with the viewer's token. Ids are now encodeURIComponent-ed; a regression test pins "../users" to its own path segment. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
44971c23e6
commit
a7609ff3f8
5 files changed
+82
-22
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import { deleteRawMaterialLot, fetchRawMaterialLot } from "./api";
|
||||
import { fetchStock } from "@/lib/stock/api";
|
||||
import { fetchProductionBatch } from "@/lib/production-batches/api";
|
||||
|
||||
vi.mock("@/lib/auth/api-client", () => ({ apiFetch: vi.fn(async () => ({})) }));
|
||||
|
||||
/**
|
||||
* Ids come from the URL (useParams). Unencoded, a crafted link such as
|
||||
* /raw-materials/..%2Fusers would make the page call another endpoint with
|
||||
* the viewer's token (client-side path traversal). Every helper must keep
|
||||
* the id inside its own path segment.
|
||||
*/
|
||||
describe("id encoding in API paths", () => {
|
||||
beforeEach(() => vi.mocked(apiFetch).mockClear());
|
||||
|
||||
const hostile = "../users";
|
||||
|
||||
it("keeps a raw material id inside its path segment", async () => {
|
||||
await fetchRawMaterialLot(hostile);
|
||||
await deleteRawMaterialLot(hostile);
|
||||
expect(vi.mocked(apiFetch).mock.calls.map(([path]) => path)).toEqual([
|
||||
"/raw-material-lots/..%2Fusers",
|
||||
"/raw-material-lots/..%2Fusers",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps stock and production batch ids inside their path segment", async () => {
|
||||
await fetchStock("production-batches", hostile);
|
||||
await fetchProductionBatch(hostile);
|
||||
expect(vi.mocked(apiFetch).mock.calls.map(([path]) => path)).toEqual([
|
||||
"/production-batches/..%2Fusers/stock",
|
||||
"/production-batches/..%2Fusers",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,9 @@ export function fetchRawMaterialLots(
|
||||
}
|
||||
|
||||
export function fetchRawMaterialLot(id: string): Promise<RawMaterialLot> {
|
||||
return apiFetch<RawMaterialLot>(`/raw-material-lots/${id}`);
|
||||
return apiFetch<RawMaterialLot>(
|
||||
`/raw-material-lots/${encodeURIComponent(id)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function createRawMaterialLot(
|
||||
@@ -27,7 +29,10 @@ export function createRawMaterialLot(
|
||||
}
|
||||
|
||||
export function deleteRawMaterialLot(id: string): Promise<{ id: string }> {
|
||||
return apiFetch<{ id: string }>(`/raw-material-lots/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return apiFetch<{ id: string }>(
|
||||
`/raw-material-lots/${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user