Security guides for developers tend to fail in one of two ways. They are either a wall of compliance language nobody reads, or a list of scary words with no instructions. This is the checklist I actually run before I ship something, written the way I would explain it to a teammate.
Authentication: stop rolling your own
If you are hashing passwords by hand in 2026, stop. Use a library that does argon2id or bcrypt with sane defaults. The number of ways to get this subtly wrong is large, and none of them show up in testing because a weak hash still logs the user in fine.
Sessions over JWTs for most web apps. A server-side session you can revoke beats a stateless token you cannot. If you do use tokens, keep them short-lived and have a real refresh flow. The convenience of “I never have to check the database” turns into a problem the first time you need to kick someone out right now.
Authorization is where the real bugs live
Authentication asks who you are. Authorization asks what you are allowed to touch, and this is where most serious breaches happen. The classic one: an endpoint reads the user ID from the request body instead of the session, so I can edit my profile by sending your ID. It is called IDOR and it is everywhere.
The fix is a habit, not a tool. Every time you load a record, ask “does the current user own this, and did I check?” Write that check at the data layer so it cannot be forgotten in a controller. The same care applies to AI features, by the way: an agent acting on a user’s behalf needs the user’s permissions, not the service account’s, a point I get into in agentic AI in cybersecurity.
Input is hostile until proven otherwise
SQL injection is old and still works because someone, somewhere, is still building queries with string concatenation. Use parameterized queries. Always. Your ORM probably does this for you, right up until you drop into a raw query for performance and forget.
For anything that ends up in HTML, the framework’s default escaping is your friend. The danger is the moment you reach for the “render this as raw HTML” function. Every XSS bug I have ever fixed lived within a few lines of one of those calls.
Secrets do not belong in the repo
API keys, database passwords, signing secrets: none of these go in git, not even in a private repo, not even “temporarily.” Use environment variables or a secrets manager. Add a pre-commit scanner so a tired version of you cannot leak one at midnight.
And rotate them when someone leaves or when a key has been sitting around for a year. A secret you cannot remember creating is a secret you should retire.
The headers most apps forget
A handful of HTTP response headers buy you a lot of safety for almost no effort. A strict Content-Security-Policy is the big one; it is annoying to tune and worth it. Add HSTS so browsers refuse to talk to you over plain HTTP, and set sensible cookie flags (HttpOnly, Secure, SameSite). These are a thirty-minute job that closes whole categories of attack.
Dependencies are your attack surface too
Most of your code is not your code. Run an audit on your dependencies, turn on automated update PRs, and actually read them instead of rubber-stamping. A compromised package in your build pipeline can do anything your build can do, which is usually a lot. This is one reason I keep build and runtime boundaries clean, something I write about in modern full-stack architecture.
Run it before you ship it
Point a scanner at your own app before an attacker does. Even a free one will catch the obvious holes. Pair that with the habit of testing the unhappy paths: what happens when I send the wrong type, a huge payload, someone else’s ID, a missing token. The bugs hide in the cases you did not plan for.
None of this is exotic. It is the same ten things, done every time, that separate apps that get breached from apps that do not. If you want to go further into building secure systems with AI in the loop, the engineering practices in practical AI engineering are the natural next read.