These are the checks that can't be enforced by rules alone. You need to actually test them. Go through each one before deploying to real users.
Take your Supabase project URL and anon key (both are in your frontend code). Make a direct REST call from outside your app:
curl https://YOUR_PROJECT.supabase.co/rest/v1/users?select=* \
-H "apikey: YOUR_ANON_KEY"PASS: Empty array or permission error. FAIL: Returns actual user data.
For Firebase: try reading a collection without being authenticated.
Log in and copy a request to a protected endpoint from browser DevTools (Network tab). Log out. Replay that exact request without the session cookie or auth header.
PASS: Returns 401. FAIL: Returns data.
Also test: call an admin-only endpoint with a regular user session.
PASS: Returns 403. FAIL: Returns admin data.
# Is .env tracked?
git ls-files .env
# PASS: No output
# Is .env in .gitignore?
grep "\.env" .gitignore
# PASS: Shows a match
# Are there secrets in source code?
grep -rn "sk_live_\|sk_test_\|AKIA\|password\s*=\s*['\"]" \
--include="*.js" --include="*.ts" --include="*.py" --include="*.jsx" --include="*.tsx" \
./src ./app ./pages ./api 2>/dev/null
# PASS: No output
# Are there secrets in git history?
# (install gitleaks: https://github.com/gitleaks/gitleaks)
gitleaks detect --source . --verbose
# PASS: No leaks foundCreate two test accounts (User A and User B). As User A:
- Find a resource ID that belongs to User B
- Request
GET /api/resources/{user_b_resource_id}with User A's session
PASS: Returns 403. FAIL: Returns User B's data.
Also test writes: PUT /api/resources/{user_b_resource_id} with User A's session should also return 403.
Test every endpoint that takes a user-scoped ID: documents, payments, profile data, orders.
Open your app in a browser. Open DevTools.
- Go to Sources tab. Search all files for:
sk_,AKIA,Bearer,secret,private_key - Go to Network tab. Watch outbound requests. Check headers for auth tokens being sent from the client to third-party APIs
PASS: No secret keys found. FAIL: Any match.
If your app has any feature that fetches a URL from user input (link previews, image proxies, URL validators), submit these:
http://127.0.0.1/
http://localhost/
http://169.254.169.254/latest/meta-data/
http://10.0.0.1/
http://[::1]/
PASS: All rejected before any request is made. FAIL: Any returns content.
If your app doesn't fetch user-supplied URLs, skip this.
Create this HTML file and open it in a browser where you're logged into your app:
<form action="https://yourapp.com/api/change-email" method="POST">
<input name="email" value="attacker@evil.com">
</form>
<script>document.forms[0].submit()</script>Replace the URL with any state-changing endpoint in your app.
PASS: Action fails (403 or no effect). FAIL: The action actually executes.
curl -I https://yourapp.com 2>/dev/null | grep -i "content-security-policy\|strict-transport\|x-frame-options\|x-content-type"PASS: All four headers present. FAIL: Any missing.
Or go to https://securityheaders.com and enter your URL.
curl -I -H "Origin: https://evil.com" https://yourapp.com/api/anything 2>/dev/null | grep -i "access-control-allow-origin"PASS: No header, or shows your specific domain only.
FAIL: Shows * or echoes back https://evil.com.
for i in $(seq 1 50); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://yourapp.com/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"wrong"}'
donePASS: Starts returning 429 after a few attempts. FAIL: All 50 return 401 without being blocked.
Submit these in every input that likely touches the database (login, search, filters):
' OR '1'='1
'; DROP TABLE users; --
PASS: No unexpected data, no database errors exposed. FAIL: Returns unexpected data, shows a SQL error, or breaks.
Submit in every text input (names, comments, bios, search, URL params):
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
PASS: Displayed as plain text. No alert fires. FAIL: Alert appears.
curl -X POST https://yourapp.com/api/webhook \
-H "Content-Type: application/json" \
-d '{"type":"payment_intent.succeeded","data":{"object":{"id":"pi_fake"}}}'PASS: Returns 400 or signature error. FAIL: Returns 200 and processes the event.
Also send the same valid event twice (same event ID). If it processes both times, idempotency is missing.
Create a file named test.jpg with this content:
<script>alert('XSS')</script>
Upload it through the normal flow. Navigate to the uploaded file's URL.
PASS: Rejected, or served as download, or on a different domain. FAIL: Content renders/executes on the same origin as your app.
Submit deliberately bad data: invalid JSON, non-existent IDs, a single ' character in text fields.
PASS: Responses say "Something went wrong" with no technical details. FAIL: Response contains stack traces, SQL queries, file paths, or library names.
Check your codebase:
grep -rn "hashlib.md5\|hashlib.sha1\|MD5\|SHA1\|createHash.*md5\|createHash.*sha1" \
--include="*.py" --include="*.js" --include="*.ts" ./PASS: No matches. FAIL: Any match means passwords may be using a broken algorithm. Should be bcrypt, Argon2, or scrypt only.
If you can access your database, check the stored hash format. bcrypt hashes start with $2b$. MD5 is 32 hex characters. SHA-1 is 40.
For every package your AI suggested that you haven't used before:
- Search for it on npmjs.com or pypi.org
- Check: does it exist? How many weekly downloads? When was it first published? Who maintains it?
Red flags: fewer than 1,000 weekly downloads, published in the last 30 days, single unnamed maintainer, name is suspiciously similar to a popular package.
npm audit
# or
pip auditPASS: No critical or high vulnerabilities.
If you only test 5 things, test these:
- #1 (database not publicly queryable)
- #2 (API routes reject unauthenticated requests)
- #3 (no secrets in git)
- #4 (can't access other users' data)
- #5 (no secret keys in browser)
Then come back and do the rest.