From bc336a9744705d59f8e91d1e24a5679babf14fdb Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Mon, 8 Dec 2025 01:02:20 +0100 Subject: [PATCH] feat: secure attachment download route with shareId validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Update attachment download link in UI to use shareId - Add shareId validation to attachment download route - Validate TTL before allowing attachment downloads - Extract locationId from shareId using extractShareId helper Security: - Attachment downloads now validate checksum and TTL - Prevents unauthorized access to bill attachment files - Returns 404 for invalid/expired share links 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/[locale]/share/attachment/[id]/route.tsx | 45 ++++++++++++++++++-- app/ui/ViewBillCard.tsx | 2 +- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/[locale]/share/attachment/[id]/route.tsx b/app/[locale]/share/attachment/[id]/route.tsx index b8099f8..a2e711c 100644 --- a/app/[locale]/share/attachment/[id]/route.tsx +++ b/app/[locale]/share/attachment/[id]/route.tsx @@ -1,12 +1,49 @@ import { fetchBillById } from '@/app/lib/actions/billActions'; import { notFound } from 'next/navigation'; +import { extractShareId, validateShareChecksum } from '@/app/lib/shareChecksum'; +import { getDbClient } from '@/app/lib/dbClient'; +import { BillingLocation } from '@/app/lib/db-types'; -export async function GET(request: Request, { params:{ id } }: { params: { id:string } }) { - const [locationID, billID] = id.split('-'); +export async function GET(request: Request, { params: { id } }: { params: { id: string } }) { + // Parse shareId-billID format + // shareId = 40 chars (locationId 24 + checksum 16) + const shareId = id.substring(0, 40); + const billID = id.substring(41); // Skip the '-' separator - const [location, bill] = await fetchBillById(locationID, billID, true) ?? []; + if (!shareId || !billID) { + notFound(); + } - if(!bill?.attachment) { + // Validate shareId and extract locationId + const extracted = extractShareId(shareId); + if (!extracted) { + notFound(); + } + + const { locationId: locationID, checksum } = extracted; + + // Validate checksum + if (!validateShareChecksum(locationID, checksum)) { + notFound(); + } + + // Check TTL before fetching bill + const dbClient = await getDbClient(); + const location = await dbClient.collection("lokacije") + .findOne({ _id: locationID }, { projection: { shareTTL: 1 } }); + + if (!location) { + notFound(); + } + + // Check if sharing is active and not expired + if (!location.shareTTL || new Date() > location.shareTTL) { + notFound(); + } + + const [_, bill] = await fetchBillById(locationID, billID, true) ?? []; + + if (!bill?.attachment) { notFound(); } diff --git a/app/ui/ViewBillCard.tsx b/app/ui/ViewBillCard.tsx index bcfe862..cdaeab6 100644 --- a/app/ui/ViewBillCard.tsx +++ b/app/ui/ViewBillCard.tsx @@ -100,7 +100,7 @@ export const ViewBillCard: FC = ({ location, bill, shareId }) attachment ?

{t("attachment")}

- + {decodeURIComponent(attachment.fileName)}