The product, its code paths and every finding below are invented to show the format. Nothing here was reproduced against a real system; the verification steps describe how a contracted audit would confirm each finding.
Product
Acme Invoices — a fictional B2B invoicing app
Reviewed
Repository at a fictional commit, plus the staging environment
Stack
Next.js App Router, Supabase (Postgres and Auth), Stripe Billing, a hosted language model for invoice extraction
In scope
Account and data boundaries, the AI extraction flow, billing webhooks, the release process
Sign-in uses the platform's hosted authentication with email verification. No change recommended.
Row-level security is enabled on every table in the exposed schema, and the invoices policies limit reads to the user's account. F1 is a server route that bypasses them, not a missing policy.
Not established
Production environment values were not shared (names only), so whether production uses the same service-role pattern as staging was not confirmed.
Behaviour under many concurrent uploads was not tested.
Extraction accuracy was not measured; that needs a set of invoices with checked answers.
How findings are ranked
Each rating combines what could go wrong with who could cause it. This is Soatech’s own working rubric, not an industry scoring standard such as CVSS; every finding states its reason, so your team can disagree on evidence.
RatingConsequenceWho can trigger it
HighConsequence: Exposes customer data or money, or blocks a safe releaseWho can trigger it: An ordinary user, or anyone on the internet, under normal conditions
MediumConsequence: Wrong data or a failure that users will seeWho can trigger it: Realistic conditions, without exposure across accounts
LowConsequence: Operational or maintenance riskWho can trigger it: Little direct effect on users today
Findings
Each finding separates what was observed in the code from the risk inferred from it.
Severity: HighF1
Invoice export can return another account's invoice
Why this rating
Customer financial data, reachable by any signed-in account without special conditions.
Where
app/api/invoices/[id]/export/route.ts
Observed
The handler loads the invoice by id with the service-role database client and checks only that a session exists. The service-role client bypasses row-level security, so the table's policies do not apply in this route.
Inferred risk
A signed-in user who obtains another account's invoice id can download that invoice. Ids are UUIDs, so guessing is unlikely, but they appear in shared links and notification emails.
Recommended fix
Use the request-scoped client so row-level security applies, or filter by the caller's account in the query. Keep the service-role client out of request handlers.
How to verify the fix
A test signs in as account B and requests account A's export; it must receive 404. Run it before the fix (it fails) and after (it passes).
Severity: HighF2
Billing webhook trusts unsigned requests
Why this rating
Money and account state, on an endpoint anyone on the internet can reach.
Where
app/api/webhooks/stripe/route.ts
Observed
The handler parses the JSON body and updates the subscription without verifying the Stripe-Signature header, and processes every delivery of an event as new.
Inferred risk
A fabricated request could mark an account as paid. Stripe can deliver the same event more than once, so a redelivery can apply the same change twice.
Recommended fix
Verify the signature against the raw request body with the endpoint secret and return 400 on failure. Record processed event ids and skip ones already handled.
How to verify the fix
With the Stripe CLI in test mode, forward events to staging: a request with an altered body returns 400, and resending an event leaves the subscription unchanged.
Severity: MediumF3
Extracted invoice fields are saved without validation
Why this rating
Wrong data reaches users under normal use; no exposure across accounts.
Where
lib/ai/extract-invoice.ts and the invoices table
Observed
The model's response is parsed and inserted directly as a live invoice. A missing total is stored as null, which the list view shows as €0.00. A response that is not valid JSON surfaces as a server error.
Inferred risk
Incorrect totals can reach a customer's records without anyone checking them.
Recommended fix
Validate the response against the required fields and types. Save results as drafts for review, hold incomplete or malformed output with the reason, and show a clear message when extraction fails.
How to verify the fix
Four fixtures — a complete response, one without a total, one with a malformed date and one that is not JSON. The first becomes a draft for review; the other three are held with their reason; none creates a live invoice.
Severity: MediumF4
Model calls have no timeout and retry every error
Why this rating
A failure users will meet under realistic conditions, such as a large PDF; it wastes model usage but exposes nothing.
Where
lib/ai/client.ts
Observed
The extraction call sets no timeout of its own, and a wrapper retries every error up to five times — including requests the provider rejected as invalid.
Inferred risk
A request that can never succeed is sent five times, the upload can outlast the hosting platform's function time limit, and the user waits for a generic error.
Recommended fix
Set a timeout below the function limit. Retry only rate limits, server errors and dropped connections, with backoff and a total time budget. Tell the user when a document is too large.
How to verify the fix
With a stubbed client: a rate-limit response followed by success completes after one retry; an invalid-request response is not retried; a call that never answers ends at the timeout.
Severity: LowF5
Schema changes are applied by hand
Why this rating
No immediate user impact, but it raises the cost and risk of every later change.
Where
Release process; supabase/migrations
Observed
The staging schema has two columns and one access policy that no migration file creates. The README describes applying changes in the database dashboard.
Inferred risk
Staging and production can drift apart unnoticed, including their access policies, and a new environment or a rollback cannot be reproduced.
Recommended fix
Capture the current schema as a migration and make further changes only through reviewed migration files. Add the step to the release checklist.
How to verify the fix
A database rebuilt from the migrations alone matches staging: a schema diff between the two is empty.
Limits of this sample, and of an audit
This sample is invented. A real report describes your product, at the version and environment that were actually reviewed, and only the areas agreed in the written scope.
The scope above is an example, not a standard package. Size, access and the areas covered are agreed in writing before any access is shared.
An audit is an engineering review. It is not a penetration test, a security certification or a legal compliance opinion, and it makes no code changes.
Reports vary in length. A short list of findings is a good result.