Application security has a reassuring property: the attacks that cause the most damage are, overwhelmingly, not novel. Year after year the same categories of weakness — broken access control, injection, misconfiguration, weak cryptography, outdated dependencies — account for the bulk of real breaches. That is good news for defenders, because it means you can prepare against a known list rather than an infinite one. This piece is a defender's checklist: the recurring risk classes and the concrete, boring habits that neutralise each. It is deliberately defensive — every example is a safe pattern, not an exploit — because the job of building secure software is about making the safe way the default way.
Broken access control
The single most consequential class of web weakness is broken access control: the system fails to properly enforce what an authenticated user is allowed to do. A user edits the ID in a URL and sees someone else's record; an ordinary account reaches an admin action because the check was only in the UI. The root cause is almost always the same — authorisation was assumed, or enforced only on the client, instead of being checked on the server for every request.
The defence is a discipline, not a feature: deny by default, and verify on the server, on every request, that this user may perform this action on this specific resource. The client's job is presentation; it can hide a button, but hiding is not enforcing. Never rely on an identifier being secret or a control being invisible.
Authorise the object, not just the action
It is not enough to check that a user is allowed to call an endpoint; you must check they are allowed to act on the specific object they named. "Can this user delete comments?" is the wrong question. "Can this user delete comment #4821?" is the right one. Missing that per-object check is how users reach data that is not theirs.
Injection
Injection happens when untrusted input is interpreted as code or commands — the classic being SQL injection, where input is concatenated into a database query and changes its meaning. The instinct many beginners have — "I'll filter out the dangerous characters" — is the wrong model, because it is a blocklist you can always be outsmarted on. The correct model is structural separation of code from data, so input is never able to change a statement's meaning in the first place.
For databases, that means parameterised queries (prepared statements): the query structure is fixed, and user input is bound as a value that can never be executed.
# WRONG — input is concatenated into the query text and can change its meaning.
db.execute("SELECT * FROM users WHERE email = '" + user_input + "'")
# RIGHT — the query is fixed; user input is bound as a parameter, never as code.
db.execute("SELECT * FROM users WHERE email = ?", (user_input,))The same principle generalises: use APIs that separate command from data, avoid constructing shell commands or query strings by hand, and treat all external input as untrusted. Where you must render user input into a page, contextual output encoding is the analogous defence against cross-site scripting — let the framework escape it rather than assembling HTML by string concatenation.
Security misconfiguration
Perfectly good software fails when it is misconfigured: verbose error pages that leak stack traces, default credentials left in place, unnecessary features and ports exposed, permissions set too broadly, security headers missing. Configuration is part of the attack surface, and it drifts as systems grow. The defences are hardening habits: ship with secure defaults, disable what you do not use, keep environments consistent, and treat configuration as something to review deliberately rather than leave to chance.
Cryptographic failures
Sensitive data is compromised less often by breaking strong cryptography than by not using it properly — transmitting secrets in the clear, storing passwords reversibly, or rolling your own scheme. Two habits carry most of the weight. First, encrypt data in transit and protect sensitive data at rest, using well-established, current algorithms rather than anything homegrown. Second, never store passwords as plaintext or with a plain hash: use a modern, deliberately slow, memory-hard password-hashing function designed for the purpose, so that even a stolen database resists cracking. The guiding rule is to use vetted, standard building blocks and let them do their job — cryptography is the wrong place for creativity.
Vulnerable and outdated components
Your application is mostly code you did not write — frameworks, libraries, transitive dependencies. A known vulnerability in any of them is a vulnerability in your product, and this supply-chain surface is one of the most commonly neglected. The defences are operational: maintain an inventory of what you depend on, track advisories, update promptly (especially for security releases), and remove dependencies you no longer need. Automated dependency scanning that flags known-vulnerable packages turns this from a memory exercise into a routine.
Authentication and session failures
Authentication is a frequent target: weak password handling, sessions that never expire, tokens exposed to scripts. Sensible, current defences include supporting multi-factor authentication, rate-limiting and slowing repeated login attempts to blunt brute forcing, and handling sessions safely — for web apps, storing session identifiers in cookies marked HttpOnly (unreadable by JavaScript) and Secure (sent only over encrypted connections), and expiring them appropriately. The goal is that a single stolen secret is not enough, and that guessing at scale is impractically slow.
Logging, monitoring, and failing safely
If you cannot see an attack, you cannot respond to it. Logging and monitoring of security-relevant events — failed logins, access-control denials, unexpected errors — is what turns a silent compromise into a detected one, provided the logs are actually reviewed and alert on the right signals. A companion principle is to fail closed: when something goes wrong — an auth check errors, a dependency is unreachable — default to denying access rather than allowing it. An error should never become an accidental open door. (And logs themselves must never capture secrets or personal data in the clear.)
The checklist
Checklist
0/8
Risk class to primary defence
| Risk class | Primary defence |
|---|---|
| Broken access control | Server-side, per-object authorisation; deny by default |
| Injection | Parameterised queries; code/data separation |
| Cross-site scripting | Contextual output encoding |
| Security misconfiguration | Secure defaults; harden and review config |
| Cryptographic failures | Standard algorithms; modern password hashing |
| Vulnerable components | Dependency inventory, scanning, prompt updates |
| Authentication failures | MFA, rate limiting, safe session cookies |
| Insufficient logging | Log and alert on security events; fail closed |
Practical takeaway
Security is less about heroics against exotic attacks and more about consistently applying a handful of defences against the common ones. Make the safe pattern the default: a data-access layer that only allows parameterised queries, an authorisation check that every endpoint must pass, a dependency scanner in the pipeline, session cookies that are HttpOnly and Secure by construction. When the secure way is the easy way — the path of least resistance for every developer on the team — you stop relying on everyone remembering to be careful, and that is what durable application security actually looks like. Treat this checklist as a starting posture and follow the authoritative sources below for the detail behind each item.
Sources & Further Reading
- 01OWASP Top 10 — OWASP FoundationThe reference list of the most critical web application security risks.
- 02OWASP Cheat Sheet Series — OWASP FoundationConcise, practical defensive guidance per topic (access control, injection, auth, and more).
- 03Web security — MDN Web DocsPractical web-platform security guidance, including cookies and headers.
- 04Secure Software Development Framework (SSDF), SP 800-218 — NISTA framework of secure software development practices.
Editorial note — A defensive-only overview organised around well-known web risk classes. All code shown is a safe pattern; no exploit code, payloads, or attack instructions are included. Risk categories are described qualitatively; no breach statistics or specific vulnerability figures are quoted.


