-
01
Secrets that reach the browser
Anything a visitor can load, a visitor can read. Keys placed in front-end config, in a client-side environment variable or inside a build that ships to the browser are public the moment you deploy. Look for key-shaped strings in the built assets, not only in the source.
check the build output, not just the code
# look for live key shapes in everything the browser downloadsgrep -rniE "sk_(live|test)|secret|api[_-]?key|bearer " build/ public/ dist/
-
02
Values the client is allowed to pick
If the browser sends the price, the total, the discount or the user id, then the browser decides them. A payment request must be built from data the server looked up itself. The tell is a route that reads something like req.body.price or req.body.total where it should have read the row it just fetched.
-
03
Permission checked on the server
Hiding an admin button is interface work, not access control. Every route that changes data needs its own check: is this caller logged in, and are they allowed to touch this exact record? The dangerous pattern is a route that checks who you are but never checks what you own, so anyone signed in can read or edit someone else's rows by changing an id.
-
04
Queries built from user input
String-built SQL is the classic, and it survives because it reads naturally. Parameterised queries and an ORM used properly remove it. The same question applies to file paths, shell commands, redirect targets and any HTML that gets rendered raw.
two shapes worth grepping for
db.query("SELECT * FROM products WHERE name LIKE '%" + q + "%'") // injectiondb.query("SELECT * FROM products WHERE name LIKE $1", ["%" + q + "%"]) // fine# and in the front end:grep -rn "dangerouslySetInnerHTML\|innerHTML\s*=" src/
-
05
The money path, step by step
Read it as a story: what happens between "customer presses pay" and "customer has a confirmed order". Four questions decide most incidents. Can the same request be replayed twice and charge twice. What happens when the charge succeeds but the next step fails. Is stock checked and reserved before the charge, or only checked. And can two buyers claim the same last item at the same moment.
-
06
Failures that fail silently
An empty catch block turns a broken payment into a blank page. The checks that matter: is there a try around the calls that can fail, is the error logged with enough context to reconstruct it, does the user see an honest message, and is there a timeout so a hanging call does not hang the request forever.
the pattern to look for
try { await charge(card) } catch (e) {} // customer charged, order unknown
-
07
Dependencies nobody looked at again
Generated projects pull in a lot, and old packages carry old published vulnerabilities. Check what is actually reachable: run the audit your package manager offers, look at what has had no release in years, and be suspicious of a dependency tree that is far bigger than the app it supports.
what is installed, and what is known to be wrong
npm audit --omit=devnpm ls --depth=0
-
08
Deploy and error hygiene
Debug mode left on, stack traces shown to visitors, an admin route without a login, a database port open to the world, no security headers. These are the findings that cost nothing to fix and are the first thing someone scanning the internet finds.
what the outside world sees in the headers
curl -sI https://your-site.example/ | grep -iE "server|content-security|x-content-type|permissions-policy"
If that list looks doable, it is: most of it is reading. What makes it hard is doing it on your own project, where you already know what the code was supposed to do and your eyes skip the parts that match the intention.