Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions backend/src/httpRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ describe("httpRouter", () => {
vi.unstubAllEnvs();
});

it("rejects a person id with a non-string pid claim", async () => {
vi.stubEnv("JWT_SECRET", "test");
const payload = mock<onRequestPayload>();
// A validly signed token whose pid is an object, not a string.
const secret = jwt.sign({ pid: {} }, "test", {
algorithm: "HS256",
});
payload.request.method = "GET";
payload.request.url = "/documents";
payload.request.headers = { cookie: `person_id=${secret}` };

await expect(httpRouter(payload, null)).rejects.toThrow();
// The object pid must be rejected, so the extracted person id is null.
const personId = vi.mocked(handleGetOwnDocumentsRequest).mock.calls[0][2];
expect(personId).toBeNull();
vi.unstubAllEnvs();
});

it("creates images", async () => {
const payload = mock<onRequestPayload>();
payload.request.method = "POST";
Expand Down
12 changes: 11 additions & 1 deletion backend/src/httpRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,17 @@ function extractPersonIdFromCookies(cookies?: string): string | null {
try {
const decoded = jwt.verify(personId, secret, {
algorithms: ["HS256"],
}) as { pid: string };
});

if (
typeof decoded !== "object" ||
decoded === null ||
typeof decoded.pid !== "string" ||
decoded.pid.length === 0
) {
return null;
}

return decoded.pid;
} catch {
console.error("JWT verification failed for person_id cookie");
Expand Down
17 changes: 17 additions & 0 deletions backend/src/model/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ describe("deleteDocument", () => {
expect(result).toBeFalsy();
expect(prismaMock.document.delete).not.toHaveBeenCalled();
});

it("should not query Prisma when the document id is not a valid UUID", async () => {
const result = await deleteDocument(prismaMock, "not-a-uuid", "secret");

expect(result).toBeFalsy();
expect(prismaMock.document.findFirst).not.toHaveBeenCalled();
});
});

describe("updateLastAccessedAt", () => {
Expand Down Expand Up @@ -328,5 +335,15 @@ describe("document ownership", () => {
expect(docs).toEqual([]);
expect(prismaMock.document.findMany).not.toHaveBeenCalled();
});

it("returns an empty list if ownerExternalId is not a string", async () => {
// Guards against a Prisma filter object reaching the where clause.
const docs = await getDocumentsByOwner(prismaMock, {
not: null,
} as unknown as string);

expect(docs).toEqual([]);
expect(prismaMock.document.findMany).not.toHaveBeenCalled();
});
});
});
6 changes: 5 additions & 1 deletion backend/src/model/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const getDocumentsByOwner = async (
prisma: PrismaClient,
ownerExternalId: string,
) => {
if (!ownerExternalId) return [];
if (typeof ownerExternalId !== "string" || ownerExternalId.length === 0) {
return [];
}
return prisma.document.findMany({
where: {
ownerExternalId: ownerExternalId,
Expand Down Expand Up @@ -107,6 +109,8 @@ export const deleteDocument = async (
documentName: string,
modificationSecret: string,
): Promise<boolean> => {
if (!isValidUUID(documentName)) return false;

try {
console.info(`Deleting document ${documentName}`);

Expand Down