Blog / Laravel
Laravel · 6 min read · June 3, 2026

How to security-review a vibe-coded app before launch

Circuit board with AI brain motif

You built it in three weeks with Cursor or Claude, it works, and real users are about to arrive. Before they do, set aside one focused half-day and run the pass below. It is the same sequence we use in the first hours of a paid audit, trimmed to what a founder can do alone with a terminal and two test accounts. No security background required — every step says what to run and what a bad result looks like.

Work in order. The early steps are the ones that turn into incidents fastest.

1. Secrets: the repo, the history, the laptop

Start with the question that has ended startups: is anything secret sitting in the repository? A leaked .env, a live Stripe key in a config file, an AWS key pasted into a service class. Check the working tree and the git history — deleting a file in a later commit does not unleak it:

git log --all --oneline -- .env # any output = .env was committed at some point grep -rnE "sk_live_|AKIA[0-9A-Z]{16}|-----BEGIN" app config resources gitleaks git . # or: gitleaks detect — scans full history

If you find anything: rotate the credential at the provider first, then clean the history. Rotating APP_KEY deserves care — it invalidates encrypted values and signed cookies, so plan a session reset.

2. Config sanity in five minutes

On the production server (or a shell into the container), one command summarizes the dangerous toggles:

php artisan about # You want to see: Environment ........ production Debug Mode ......... OFF # Caching all "CACHED" is a bonus, not a security issue

Then probe from outside, as an attacker would:

curl -s -o /dev/null -w "%{http_code}\n" https://yourapp.com/.env # 404 or 403 curl -s -o /dev/null -w "%{http_code}\n" https://yourapp.com/telescope # 404 or 403, never 200 curl -s -o /dev/null -w "%{http_code}\n" https://yourapp.com/horizon # same

A 200 on Telescope is a full leak of requests, jobs, mail and queries. Gate it or remove it from production builds. The complete production-settings sweep (sessions, cookies, CORS, headers) is its own article: Turning off the footguns.

3. The two-account authorization sweep

This step finds the bugs that actually breach vibe-coded apps. Create two ordinary accounts, A and B. Log in as A, perform every meaningful action, and collect the URLs and API calls. Then replay them as B, swapping in A's IDs:

  • GET /invoices/17 — can B read A's invoice?
  • PUT /api/projects/9 — can B rename A's project?
  • DELETE /uploads/123 — can B delete A's file?
  • Any admin URL you know exists — can a plain user open it?

Anything that succeeds is an insecure direct object reference (IDOR) — the highest-frequency critical bug in AI-generated Laravel, because assistants add auth middleware and stop there. The fix pattern (policies plus Gate::authorize) is covered in Top Laravel vulnerabilities in AI-generated code. To see the attack surface in one place:

php artisan route:list --except-vendor

Read every POST/PUT/PATCH/DELETE row and ask: where is the line of code that checks ownership, not just login?

4. Input handling: the four greps

Four patterns account for most injection and mass-assignment findings. Grep for all of them:

grep -rn "->all()" app/Http # mass assignment risk grep -rnE "DB::raw|whereRaw|orderByRaw|selectRaw" app # SQL injection risk grep -rn "{!!" resources/views # XSS risk grep -rn "redirect(\$request" app # open redirect risk

For each hit, trace the variable: does user input reach it without validation? ->all() into an update() is a finding. DB::raw('count(*) as total') with no variables is fine. When in doubt, rewrite toward $request->validate() and query bindings — the safe patterns are in Why AI loves raw queries.

5. Uploads and storage

If the app accepts files, check three things: validation rules exist (image/mimes/max), stored names are Laravel's hashed names rather than getClientOriginalName(), and nothing private lives under public/. Then look at what is already there:

ls -la public/uploads public/storage 2>/dev/null grep -rn "getClientOriginalName" app

Anything a user uploaded that another user should not see must be served through a controller with an authorization check or a temporary signed URL — never by a guessable public path.

6. Dependencies

Two commands, two minutes:

composer audit # known CVEs in PHP deps — fix every "high"+ npm audit --omit=dev # same for frontend deps that ship to users composer outdated --direct # majors several versions behind = risk debt

Vibe-coded apps often pin whatever version the assistant's training data remembered. You do not need everything bleeding-edge before launch; you do need zero known-vulnerable packages.

7. Abuse controls on the expensive doors

Confirm a throttle exists on: login, registration, password reset, OTP/2FA checks, contact forms, and anything that sends email/SMS or hits a paid API. The test is empirical — hit the endpoint 30 times with curl and expect 429s:

for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code}\n" -X POST https://yourapp.com/login \ -d "email=probe@example.com&password=wrong"; done | sort | uniq -c # expect mostly 429 after the first few

All 200/422? Add a named limiter and throttle: middleware — it is six lines.

8. Webhooks and CSRF exemptions

Assistants love excluding routes from CSRF to make integrations work, and skipping webhook signature checks to make tests pass. Review both lists:

grep -rn "validateCsrfTokens" bootstrap/app.php # Laravel 11+ grep -rn "\$except" app/Http/Middleware # older apps grep -rn "constructEvent\|verifyHeader" app # Stripe signature check present?

Every CSRF-exempt route must verify a signature or token some other way. An unverified /webhooks/stripe endpoint means anyone can POST "payment succeeded" to your app.

9. Errors and logging

Trigger an error on purpose (a 500 on a junk route, a failed validation, a wrong password five times) and check two places: what the user saw (it must not be a stack trace) and what the log recorded. You want failed logins and 500s visible in logs, secrets absent from them, and something — even a free Slack webhook on the critical channel — that tells you when production is on fire. Silent failure is how a breach goes unnoticed for months.

10. Triage what you found

Sort findings into three buckets and act accordingly:

  • Block launch: exposed secrets, debug mode on, any IDOR, SQL injection, unauthenticated admin routes.
  • Fix within the first week: missing throttles, XSS in user content, upload validation, known-vulnerable dependencies.
  • Schedule: header hardening, logging/alerting polish, dependency majors.

Re-run the relevant check after each fix — a security fix you did not verify is a hope, not a fix.

When to bring in an outside review

This checklist catches the mechanical 70%. What it cannot catch is the business-logic layer: tenant isolation in your data model, privilege boundaries, race conditions around payments. If the app handles money, health data, or other people's customers, that is the point of a fixed-price secure code review — senior eyes, every finding with a fix, sample report here.

FAQ

How long does this self-review actually take?

Three to five focused hours for a typical MVP: the greps and config checks take minutes; the two-account authorization sweep is where the time goes — and where the findings are. Budget more if you have a large API surface.

Can I just run a scanner instead?

Scanners are good at config and dependency issues (steps 2 and 6) and nearly blind to authorization logic (step 3), which is where vibe-coded apps actually fail. Run a scanner as a supplement, not a substitute.

What single finding should absolutely stop a launch?

A working IDOR on customer data. It needs no skill to exploit, it is usually a reportable data breach the moment a stranger triggers it, and it is almost always systemic — one missing policy means many.

Want senior eyes before launch?

Book a fixed-price review and launch knowing what an attacker would find first.