Sample Audit Report: DemoNotes
Full Audit · demo app · tested October 29, 2026
- App
- DemoNotes (demo app built by AI App Check)
- URL audited
- A local copy of the app (not deployed)
- Tier
- Full Audit ($499)
- Audit date
- October 29, 2026
- Sign-off
- Reviewed and signed off by our QA lead
Verdict
8 findings. The critical ones let anyone see customer data and pay $0.01 instead of $19.
Fix these first
- Your Stripe secret key is visible to every visitor (F-01). Anyone can copy it from the page code and use your Stripe account. Move it to the server and rotate it.
- Customer names, emails and phone numbers can be read by anyone (F-02, F-03). The profiles table and the orders API answer without a login.
- Shoppers can set their own price (F-04). Changing one number in the browser made the $19 plan cost $0.01 at checkout.
How we rate problems
| Severity | Means |
|---|---|
| S1 critical | Data exposed, login can be bypassed, or payments broken. Fix before launch. |
| S2 major | A core flow is broken or unsafe. Fix before launch if you can. |
| S3 minor | Cosmetic or hardening. Fix soon after launch. |
Scope
- Flows tested: signup, login, wrong password, logout, logged-out access to the dashboard, password reset request, contact form, upgrade checkout (Stripe test mode) with 4 test cards, a same-site link crawl (7 pages), console errors and accessibility on 4 public pages, phone-width layout.
- Security checks: checks 1–7 of the AI App Check security checklist (see Security checks below).
- Browsers and sizes: Chrome desktop and an iPhone 14 screen size (run on Chromium, not real Safari).
- Accounts: one supplied test account plus a fresh account per payment test.
- Out of scope: the password-reset email link (no test inbox), real Safari, Firebase, load testing, a full penetration test.
Findings
| ID | Severity | Title | Where |
|---|---|---|---|
| F-01 | S1 | Stripe secret key visible in the page code | /static/config.js, loaded on every page |
| F-02 | S1 | Customer profiles readable by anyone | database table profiles |
| F-03 | S1 | Orders list works without logging in | /api/orders |
| F-04 | S1 | Price can be changed in the browser | Pricing page → "Upgrade to Pro" |
| F-05 | S2 | Script error on the pricing page | /pricing |
| F-06 | S2 | No limit on wrong-password attempts | login (/api/login) |
| F-07 | S3 | Security headers missing | every page |
| F-08 | S3 | "About us" link is broken | top menu, every page |
In a paid audit, every finding comes with a fix prompt you can paste into Lovable, Bolt, Replit or Cursor. This sample shows the first one in full (F-01); the rest are kept for clients.
S1 F-01 · Stripe secret key visible in the page code
Where: /static/config.js, loaded on every page
What happens: the file sent to every visitor's browser contains a Stripe secret key (sk_test_…).
What should happen: secret keys stay on the server; the browser only ever gets the publishable pk_ key.
Why it matters: anyone who opens the page code can use the key to read customers and charges, or issue refunds, in your Stripe account.
Steps to reproduce:
- Open any page and view the page source.
- Open /static/config.js and search for
sk_.
Evidence: Playwright trace, test "no secret keys in the browser bundle" (key redacted to its first 12 characters). Found by security.spec.ts.
Fix prompt (paste into your AI builder)
Please fix this problem in my app. PROBLEM: My Stripe secret key is inside a file every visitor's browser downloads (/static/config.js, the "stripeSecretKey" setting). IT SHOULD: Keep the Stripe secret key only on the server. The browser should only ever get the publishable key that starts with pk_. WHERE: /static/config.js and any code that calls Stripe. DO NOT: change the design, the prices, or how checkout looks. SECURITY RULES: never put secret keys in front-end code; keep them in server-side environment variables, and call Stripe only from a server function. WHEN DONE: list the files you changed, explain the fix in one sentence, and remind me to rotate (replace) the old secret key in my Stripe dashboard. I WILL CHECK IT BY: opening my site, viewing the page source and /static/config.js, and searching for "sk_". It should find nothing.
S1 F-02 · Customer profiles readable by anyone
Where: database table profiles
What happens: using only the public key that ships with the app, a logged-out visitor can read rows from profiles (columns: id, email, full_name, phone).
What should happen: each user can read only their own profile; logged-out visitors get nothing.
Why it matters: anyone can download your customers' names, emails and phone numbers.
Steps to reproduce:
- Take the project URL and public (anon) key from the app's page code.
- Request one row from
profileswithout logging in.
Evidence: output of our read-only database check (column names only; no row values recorded). The notes table returned no rows, so it is protected. Found by supabase-rls-check.ts.
S1 F-03 · Orders list works without logging in
Where: /api/orders
What happens: a request with no login gets status 200 and a list of every order, with customer emails and totals.
What should happen: the route returns 401 when nobody is logged in, and only the logged-in user's own orders otherwise. (/api/me already does this correctly.)
Why it matters: anyone can see every customer's orders.
Steps to reproduce:
- Open a private browser window (not logged in).
- Go to /api/orders.
Evidence: Playwright trace, test "private API routes reject logged-out requests" (reported 200 /api/orders). Found by security.spec.ts.
S1 F-04 · Price can be changed in the browser
Where: Pricing page → "Upgrade to Pro"
What happens: the page sends the price (price_cents=1900) from the browser and the server trusts it. Changing it to 1 made the Stripe checkout page show $0.01 instead of $19.00.
What should happen: the server decides the price; anything the browser sends about price is ignored.
Why it matters: anyone can buy the Pro plan for one cent.
Steps to reproduce:
- Open /pricing and change the hidden price field to 1 (browser developer tools).
- Click "Upgrade to Pro" and read the total on the checkout page.
Evidence: Playwright trace and page snapshot, test "price cannot be changed from the browser" ("Total due $0.01"). Found by checkout.spec.ts (Stripe test mode).
S2 F-05 · Script error on the pricing page
Where: /pricing
What happens: when the page loads, the browser reports initPricingAnalytics is not defined. The checkout button still worked in our tests.
What should happen: the page loads with no errors in the browser console.
Why it matters: a script that crashes on your money page can stop tracking, or break the page in some browsers. Rated S2 per our scale (console errors on a key page).
Steps to reproduce:
- Open /pricing.
- Open the browser console (right-click → Inspect → Console).
Evidence: Playwright trace, test "no console errors on /pricing". Home, login and signup had no errors. Found by console.spec.ts.
S2 F-06 · No limit on wrong-password attempts
Where: login (/api/login)
What happens: 15 wrong passwords in a row for the test account were all answered with 401; none was slowed down or blocked (no 429).
What should happen: after about 5 failed tries in a minute, the app blocks further tries for a while and says so.
Why it matters: without a limit, a script can keep guessing passwords for your users' accounts.
Steps to reproduce:
- On the login page, enter the test email with a wrong password 15 times.
- Every attempt is accepted for checking; none is blocked.
Evidence: test "login is rate limited" (statuses: 401 × 15). Found by security.spec.ts (15 requests, test account only).
S3 F-07 · Security headers missing
Where: every page
What happens: none of the 5 standard security headers is sent: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, frame protection (X-Frame-Options or CSP frame-ancestors), Referrer-Policy.
What should happen: every page sends all 5.
Why it matters: they make some attacks much harder, like your site being loaded inside someone else's page to trick users into clicking.
Steps to reproduce:
- Open any page, then browser developer tools → Network.
- Click the page request and read its response headers.
Evidence: test "security headers and HTTPS". The http-to-https half of this test wasn't judged: the demo ran locally, without https. Found by security.spec.ts.
S3 F-08 · "About us" link is broken
Where: top menu, every page
What happens: "About us" goes to /about-us, which returns "page not found" (404).
What should happen: the link opens an About page, or is removed.
Why it matters: visitors who click it hit a dead end, which looks unfinished.
Steps to reproduce:
- Click "About us" in the top menu.
Evidence: test "no broken links" (7 pages crawled, 1 broken: 404 /about-us). Found by links.spec.ts.
Security checks
| # | Check | Result | Finding |
|---|---|---|---|
| 1 | Secret keys in the browser bundle | Fail | F-01 |
| 2 | Exposed .env, .git or source maps | Pass | — |
| 3 | Supabase tables open to anyone | Fail profiles; notes passed | F-02 |
| 4 | Firebase rules open | Not tested app doesn't use Firebase | — |
| 5 | API routes without auth | Fail /api/orders; /api/me passed | F-03 |
| 6 | Security headers, HTTPS, cookies | Fail headers. HTTPS redirect not tested (local copy). Session cookie is HttpOnly + SameSite=Lax (checked by hand; Secure flag not judged on local http) | F-07 |
| 7 | Rate limiting | Fail login | F-06 |
| — | Price set by the server, not the browser | Fail | F-04 |
Also passed: signup, login, wrong password rejected, logout, logged-out visitors sent to login, password reset gives the same message for unknown emails, contact form blocks an empty submit, no critical or serious accessibility issues on 4 pages, no sideways scrolling on a phone. Checkout with test cards: a payment unlocks Pro; the success page alone unlocks nothing; declined, 3D Secure and insufficient-funds cards each show an error and unlock nothing.
Your test suite
With a Full Audit you keep the Playwright suite we ran: 62 automated tests in 9 files, run on Chrome desktop and a phone screen size. Last run on the demo: 45 passed, 14 failed (the 7 automated findings × 2 sizes), 3 skipped by design. After each fix, run npx playwright test (or the included GitHub Actions workflow) and the matching test should turn green.
Limits
This is a best-effort review of one copy of the app at one point in time, not a security guarantee or a penetration test. Checkout was tested in Stripe test mode only; for this demo, Stripe's hosted page was simulated locally. Real Safari, Firebase and the password-reset email link were not tested. A retest after fixes was not done for the demo.
Want a report like this for your app?