# Manual Security Checklist 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. --- ## 1. Database is not publicly queryable Take your Supabase project URL and anon key (both are in your frontend code). Make a direct REST call from outside your app: ```bash 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. --- ## 2. API routes reject unauthenticated requests 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. --- ## 3. No secrets in git ```bash # 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 found ``` --- ## 4. Can't access another user's data by changing an ID Create two test accounts (User A and User B). As User A: 1. Find a resource ID that belongs to User B 2. 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. --- ## 5. No secret keys visible in the browser Open your app in a browser. Open DevTools. 1. Go to Sources tab. Search all files for: `sk_`, `AKIA`, `Bearer`, `secret`, `private_key` 2. 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. --- ## 6. SSRF: internal URLs are blocked 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. --- ## 7. CSRF: cross-origin form submissions are blocked Create this HTML file and open it in a browser where you're logged into your app: ```html
``` Replace the URL with any state-changing endpoint in your app. PASS: Action fails (403 or no effect). FAIL: The action actually executes. --- ## 8. Security headers are present ```bash 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. --- ## 9. CORS isn't wide open ```bash 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`. --- ## 10. Login can't be brute-forced ```bash 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"}' done ``` PASS: Starts returning 429 after a few attempts. FAIL: All 50 return 401 without being blocked. --- ## 11. SQL injection doesn't work 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. --- ## 12. XSS doesn't execute Submit in every text input (names, comments, bios, search, URL params): ```