Validra

Sample report. The application is fictional; the findings are adapted from real, anonymized engagements, with details changed.

Sample report

What you actually receive at the end of a review.

This is the structure of every Validra report, filled in with realistic findings. Evidence is redacted here the way it would be before sharing a report outside your team.

Application
Acme Billing — B2B invoicing SaaS (Next.js + Supabase)
Agreed scope
Web app, REST API, Supabase database and storage, Stripe webhooks
Access used
Read-only repository access, staging environment, two test accounts
Review period
5 business days, then one retest after fixes
Reviewer
Téo Brondel, Validra

1. Executive summary

1

Critical

2

High

1

Medium

1

Low

The review found one critical issue: any logged-in user could read and modify other customers' transactions by calling the database API directly. Two high-severity issues exposed session tokens in server logs and internal documents in a public storage bucket.

The critical and the token issue were fixed during the engagement and confirmed at retest. The storage issue is partially fixed: the bucket is now private, but previously public files should be treated as exposed.

Priority for the team: finish the storage fix, then add rate limiting on login and password reset.

How findings are rated

Severity — how bad it is for your business

  • CriticalDirect access to other users' data or money, exploitable now.
  • HighSerious exposure, but needs a condition (a leaked log, a guessed URL).
  • MediumMakes an attack easier or more likely; fix soon.
  • LowHardening; fix when convenient.

Certainty — how sure we are

  • Demonstrated — Reproduced on your environment, with evidence.
  • Weakness — A real gap in a defense, without a full exploit shown.
  • Observation — Worth knowing, not a vulnerability on its own.

2. Findings at a glance

IDFindingSeverityCertaintyStatus after retest
F-01Any logged-in user can read and modify other customers' transactionsCriticalDemonstratedFixed
F-02Session tokens written in plain text to server logsHighDemonstratedFixed
F-03Internal documents downloadable without authenticationHighDemonstratedPartially fixed
F-04No limit on login and password reset attemptsMediumWeaknessOpen
F-05Missing Content-Security-Policy headerLowObservationOpen
F-01CriticalDemonstrated

Any logged-in user can read and modify other customers' transactions

Surface: Database access control (Supabase Row-Level Security)

What we found

The transactions table had Row-Level Security enabled, but its SELECT and UPDATE policies only checked that the caller was authenticated, not that the row belonged to them. The UI filtered correctly, which hid the problem; calling the REST API directly returned every customer's rows.

Evidence — request made with test account B (redacted)

curl "https://[project].supabase.co/rest/v1/transactions?select=*" \
  -H "apikey: [anon key]" \
  -H "Authorization: Bearer [account B session]"

# 200 OK — 1,248 rows, including rows owned by account A
# and by 37 other organizations

Business impact

Every customer's financial data was readable, and editable, by any user with an account, including a free trial account.

Recommendation

Restrict both policies to rows owned by the caller's organization, and add a test that fails if another account can read a row.

create policy "org members read own transactions"
on transactions for select
using (org_id in (
  select org_id from memberships where user_id = auth.uid()
));

Retest

Fixed. The same request with account B now returns only account B's organization rows.

F-02HighDemonstrated

Session tokens written in plain text to server logs

Surface: Authentication and sessions

What we found

The invoice export link passed the user's session token (JWT) as a URL parameter. Full URLs, token included, were stored in the hosting provider's request logs and in browser history.

Evidence — log line from staging (redacted)

GET /export?invoice=[id]&token=eyJhbGciOi…[redacted] 200

Business impact

Anyone with access to the logs, or to a shared computer's history, could reuse a valid session and act as that user until the token expired.

Recommendation

Send the token in the Authorization header (or rely on the httpOnly session cookie), shorten token lifetime, and purge existing logs that contain tokens.

Retest

Fixed. Export now uses the session cookie; no token appears in URLs or new logs.

F-03HighDemonstrated

Internal documents downloadable without authentication

Surface: Storage permissions

What we found

Signed contracts uploaded by customers were stored in a public bucket, under predictable paths based on the invoice number.

Evidence — unauthenticated request (redacted)

curl -I "https://[project].supabase.co/storage/v1/object/public/contracts/INV-2026-0142.pdf"

# HTTP/2 200 — no session, no token

Business impact

Confidential customer contracts were accessible to anyone who guessed or found a URL.

Recommendation

Make the bucket private, serve files through short-lived signed URLs checked against the user's organization, and rename existing files so old URLs stop working.

Retest

Partially fixed. The bucket is now private and new links are signed. Existing files keep their old names, so previously shared URLs should be considered exposed.

F-04MediumWeakness

No limit on login and password reset attempts

Surface: Authentication

What we found

The login and password reset endpoints accepted 50 consecutive attempts from the same IP without delay or block. No account was compromised during the test.

Observation during testing

50 failed logins in 40 seconds from one IP → no rate limit, no lockout, no alert

Business impact

Makes password guessing and reset-email flooding practical.

Recommendation

Add rate limiting per IP and per account on both endpoints, with a short progressive delay.

Retest

Open. Planned by the team for the next sprint.

F-05LowObservation

Missing Content-Security-Policy header

Surface: Configuration

What we found

The app sends no Content-Security-Policy header. No injection point was found during the review.

Observation

Response headers: no content-security-policy

Business impact

Not a vulnerability by itself, but it would limit the damage of a future script injection.

Recommendation

Add a CSP, starting in report-only mode to avoid breaking the app.

Retest

Open. Low priority.

3. Scope and limits

Tested

  • Row-Level Security on all tables exposed through the API
  • Authorization on API routes and server actions, with two test accounts
  • Login, session, and password reset flows
  • Secrets in the repository and in the shipped JavaScript bundle
  • Storage bucket permissions
  • Stripe webhook signature verification

Not tested

  • Hosting infrastructure and network
  • Denial of service and load
  • Mobile apps and third-party services beyond their integration points
  • Anything outside the agreed scope — a review only reports on what was tested

4. Shareable engagement summary

A short, non-technical block you can paste into a customer security questionnaire.

Acme Billing had an external security review by Validra (Téo Brondel), independent from the Acme team.

Scope: web app, API, database access controls, storage, authentication, Stripe webhooks. Method aligned with the OWASP Top 10, with manual testing between two test accounts.

Result: 5 findings (1 critical, 2 high, 1 medium, 1 low). Critical and high issues fixed or mitigated and verified at retest.

This summary describes a review performed at a given time within an agreed scope. It is not a certification (such as SOC 2 or ISO 27001) and does not guarantee the absence of other vulnerabilities.

Want this report for your own app?

€395 to start, about 5 business days. €395 more only if the review finds security issues relevant to the agreed scope: €790 at most.