PromptBase
Upgrade
Close icon
General
Home
Marketplace
Create
Hire
Login
Chat
Sell
Explore

Skill Details

Runtime
(claude-5-sonnet)
Example user message
[userMessage]: Our pentest report flagged a Server Action and I do not fully understand the finding. This is the whole thing, in app/invoices/actions.ts: "use server" export async function markPaid(formData: FormData) { const id = formData.get("invoiceId") await prisma.invoice.update({ where: { id: id }, data: { paidAt: new Date() }, }) revalidatePath("/invoices") } The form that calls it only renders on the invoice detail page, and that page already redirects anyone who is not signed in. So I assumed the session check was covered. The report says any authenticated user can mark any invoice paid, including invoices belonging to other companies, by replaying the request with a different id. Is that actually true given the page guard? And if I add a check, does it go in the page or in the action? Next 15, App Router, Prisma, multi-tenant with an organizationId column on every table.
Example agent response
## Scope app/invoices/actions.ts, one Server Action performing a write. The page guard on the invoice detail page is irrelevant to this file: it protects a render, not an endpoint. Yes, the finding is real. ## Findings 1. app/invoices/actions.ts — Server Actions rule 1. The action is a public HTTP endpoint. Next.js compiles it to a POST route with a generated action id. Anyone holding a session cookie can post to that id directly with curl. The page never runs, so the page guard never runs. 2. app/invoices/actions.ts — Server Actions rule 5. The write is scoped by invoice id alone. Nothing ties the row to the caller's organization, which is the cross-tenant part of the finding. 3. app/invoices/actions.ts — Server Actions rule 2. `formData.get` returns a string, a File, or null. That value goes straight into the query. A null reaches Prisma as a null filter. 4. app/invoices/actions.ts — Caching rule 2. `revalidatePath` on the whole invoices path drops more cache than this write touched. ## Patch Authorization moves into the query itself rather than being a separate read-then-write, which would leave a window between the check and the update: "use server" import { z } from "zod" import { revalidateTag } from "next/cache" import { getSession } from "@/lib/session" import { prisma } from "@/lib/prisma.server" const MarkPaidInput = z.object({ invoiceId: z.string().uuid(), }) export async function markPaid(formData: FormData) { const session = await getSession() if (!session) { return { ok: false, error: "Not signed in." } } const parsed = MarkPaidInput.safeParse({ invoiceId: formData.get("invoiceId"), }) if (!parsed.success) { return { ok: false, error: "That invoice id is not valid." } } const result = await prisma.invoice.updateMany({ where: { id: parsed.data.invoiceId, organizationId: session.organizationId, paidAt: null, }, data: { paidAt: new Date() }, }) if (result.count === 0) { return { ok: false, error: "Invoice not found or already paid." } } revalidateTag("invoices") return { ok: true } } Three things changed beyond the session check. `updateMany` with the tenant column in the where clause makes authorization part of the write, so there is no gap between checking and updating. The `paidAt: null` condition makes a replayed request a no-op instead of a second payment record. And the error message is deliberately identical for a wrong id and another company's id, so the endpoint does not confirm which invoice numbers exist. Add `import "server-only"` to lib/prisma.server.ts while you are in there. It is the rule that stops a future refactor from pulling the client into a browser bundle. ## Follow-ups - Every other action in this codebase has the same shape. Grep for `formData.get` and check each one for a tenant column in the where clause. The pentest found one because they stopped at one. - Tag the invoice fetches with the `invoices` tag, otherwise the revalidation call has nothing to invalidate. - The form needs to render the returned error. Wire it through `useActionState` so a failed write is visible instead of silent. - This review cannot confirm the fix. Replay the original curl request against a staging build with a second organization's invoice id and confirm you get the not-found result.
Add to Cart
PromptBase Select
Or get it with Select10 Select prompts/mo — under $2 each
Learn more
Instant accessInstant access
Usage rightsCommercial use
Money-back guaranteeMoney‑back
By purchasing this skill, you agree to our terms of service
CLAUDE-5-SONNET
Tested icon
Guide icon
Free credits icon
Cursor rule pack for reviewing React and Next.js App Router code: server versus client component boundaries, use client placement, hydration mismatch sources, Server Action input validation, secret leakage across the RSC boundary, Content-Security-Policy nonce wiring, Suspense and streaming placement, revalidate and cache choices, and client bundle weight. It writes rules, findings, and patches only, and does not run your build, start a dev server, or deploy.
...more
Added 2 weeks ago
Report
Browse Marketplace