Vibe Coding Security Review — What to Test Before You Ship
A successful login and a successful test payment can make a release feel ready. Security bugs tend to appear in the requests that should fail. This is a practical pre-release review, not a substitute for a professional security assessment.
Run active tests only against staging systems you own or have explicit permission to test. Use the payment provider’s test mode.
Check whether server secrets reached the browser
Open the deployed site and inspect Sources, Network, and Storage in the browser developer tools. Some values, such as a Supabase publishable key, are designed to be public. sb_secret_..., legacy service_role, Stripe sk_live_... and whsec_... values, and private keys must never be present in the browser.
In Next.js, a statically referenced value such as process.env.NEXT_PUBLIC_NAME is inlined into the browser bundle at next build time. It remains fixed if that build is later promoted to another environment. A dynamic lookup such as process.env[varName] is not automatically inlined, but that is not a safe way to hide a secret. Do not give server-only values the NEXT_PUBLIC_ prefix, and read them only in server code.
The absence of a .env file in the current directory proves very little. Search source, generated bundles, Git history, CI/CD, deployment-platform environment variables, and logs. This pattern is only a starting point; use a dedicated secret scanner and GitHub alerts as well.
rg -l --hidden --glob '!.git/**' --glob '!node_modules/**' \
'(sb_secret_|SUPABASE_SERVICE_ROLE_KEY|service_role|sk_(live|test)_|sk-proj-|gh[pousr]_|github_pat_|AKIA[0-9A-Z]{16}|whsec_|BEGIN ([A-Z ]+ )?PRIVATE KEY)' .
That command checks only the current checkout and reports file names rather than matching values. Scan the full Git history separately with a tool that does not print raw values, or use GitHub secret scanning. If a real secret leaked, revocation comes before cosmetic cleanup: issue a replacement, migrate every consumer, disable the old credential, and then address repository history. Never paste a complete secret into a search report or issue.
Cross the authorization boundary with two users
Create test accounts A and B, then compare:
- a signed-out request and a request with an invalid token
- account A requesting its own resource
- account A placing account B’s resource ID in the URL or request body
- a regular user requesting an admin-only field or action
Hiding a button is not authorization. The server and database must validate the session, role, and object ownership on every request.
With Supabase, review Data API exposure, the GRANT privileges held by anon and authenticated, and all existing RLS policies together. RLS selects allowed rows; it does not conceal columns. Put email addresses, internal notes, and other restricted fields behind column privileges, a safe view that exposes only required fields, or a server API. Existing permissive policies can combine with new policies using OR, so also look for broad rules such as using (true).
Review CORS and CSRF separately
CORS controls how a browser can read a cross-origin response. It is not authentication or authorization, and it does not prevent every request from being sent. Return Access-Control-Allow-Origin only for an Origin that exactly matches your allowlist. Use Access-Control-Allow-Credentials: true only where cookies are required. If the response headers vary by request origin, send Vary: Origin so caches do not mix responses for different origins.
For cookie-authenticated POST, PUT, PATCH, and DELETE requests, use the framework’s CSRF protection or a proven CSRF-token pattern, and validate Origin. A read-only GET must not change state. Secure, HttpOnly, and an appropriate SameSite setting strengthen a session cookie, but they do not usually replace a CSRF token.
An externally called endpoint such as a Stripe webhook may be excluded from the normal CSRF-token check. Keep that exception narrow and require valid Stripe signature verification on the route.
Validate input when you accept it and when you render it
Storing the literal string <script> does not by itself prove a vulnerability. Validate length, type, format, and range on the server, then encode stored values for their output context—HTML, URL, JavaScript, or another sink.
On staging, submit special characters, oversized values, and boundary values. Check that they remain inert text everywhere else they appear. Also check that error responses do not disclose SQL, internal file paths, or stack traces.
Test payments from the raw webhook through duplicate delivery
A user can modify price and quantity values sent by the browser. Recalculate them on the server from a trusted product ID and enforce allowed quantities there. Change the order state only after validating the payment provider’s webhook, not because the browser showed a success message.
Stripe signature verification requires the raw request body before parsing or reserialization, the Stripe-Signature header, and the signing secret for that endpoint. Confirm that a global JSON parser does not alter the body before the webhook route receives it.
Return a 2xx quickly after the minimum synchronous work, such as signature verification and queue insertion, then move slow work to an asynchronous job. Webhooks can be delivered more than once. Record processed event.id values; because separate events may still refer to the same object, also inspect event.type and data.object.id. Use database uniqueness constraints and transactions so an order or payment update has one effect even when it runs again. If your server retries a Stripe API POST, reuse the same idempotency key for the same operation.
In test mode, exercise a changed amount, an invalid signature, redelivery of the same event, out-of-order delivery, and a retry after downstream processing fails.
Rate-limit sensitive actions and store passwords deliberately
Registration, login, password reset, verification codes, and paid APIs need request limits. An IP-only limit handles neither shared networks nor distributed attacks well. Design limits by account, IP, and operation, then verify the 429 response and user-facing message.
If you must store passwords yourself, OWASP’s first choice is Argon2id. Consider scrypt where Argon2id is unavailable; reserve bcrypt for compatibility with legacy systems. Plaintext, reversibly encrypted passwords, and fast general-purpose hashes are not appropriate. Managed authentication still leaves application authorization, session expiry, and login redirect settings for you to review.
Keep investigative signals, not secrets, in logs
Search browser consoles and server logs for Authorization and Cookie headers, access and refresh tokens, session IDs, passwords, database connection strings, private keys, and payment-card data. Do not log those values. Mask or hash identifiers where a stable reference is required. For personal data such as email addresses, define the operational need, retention period, and access control, then keep only what is necessary.
Do log authentication failures, authorization denials, key or permission changes, and payment-state transitions consistently. Time, outcome, and an internal user or request ID can provide an audit trail without exposing a credential. Review who can read the logs and how long they are retained.
Repeat the same failing requests after the fix
A leaked secret should be revoked immediately. Changes that must move together—RLS, GRANT, and application code, for example—need a more controlled release. Validate the set on staging, document a short deployment sequence, and prepare a rollback path.
After deployment, rerun the signed-out case, accounts A and B, CSRF checks, and successful, failed, and duplicate payment flows. Passing this list does not prove that the product has no vulnerabilities. If a public service handles personal data or payments, commission an independent review with a defined scope.
Official references
- Next.js environment variables
- Securing the Supabase API
- Supabase Row Level Security
- OWASP API Security Top 10 (2023)
- MDN CORS guide
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- OWASP Input Validation Cheat Sheet
- Stripe webhook signature verification
- Stripe webhook best practices
- Stripe idempotent requests
- OWASP Password Storage Cheat Sheet
- Removing sensitive data from a GitHub repository
- GitHub secret scanning
- OWASP Logging Cheat Sheet