Web Application Security Fundamentals Every Developer Should Know
A small number of mistakes account for most real breaches, and all of them are preventable.

A small number of mistakes account for most real breaches, and all of them are preventable.

Security has an unhelpful reputation for being arcane — a specialism involving assembly language and adversaries in hoodies. The reality of most breaches is considerably more mundane, and considerably more preventable.
A URL with a numeric ID that nobody checks against the logged-in user. A form value interpolated into a query. A credential committed to a repository three years ago. A dependency with a known vulnerability and no upgrade path.
These are the ones that matter. None require deep expertise to prevent, and all of them are still ordinary today.

This is the plain failure to check whether the person making a request is allowed to do what they asked. It is consistently the most frequently found serious vulnerability in real applications.
The classic shape: a URL like /invoices/1042 loads an invoice by ID and never verifies it belongs to the signed-in user. Change the number and you are reading someone else's data. No exploit, no tooling — just editing a URL.

The rule is unglamorous and absolute. Every request that touches a resource must verify both authentication (who is this?) and authorisation (may they do this, to this specific thing?). Hiding a button in the interface is not authorisation, because the interface is not what enforces anything.
Deny by default. Access should be refused unless a rule explicitly permits it, so a forgotten check fails closed rather than open. A permissive default means the missing check is invisible until someone finds it.
Injection happens when user input is treated as code rather than as data. SQL injection is the famous case, but the same pattern applies to shell commands, template rendering and any interpreted context.
The fix has been the same for two decades and it works completely: use parameterised queries. Sending the query structure and the values separately means the database never confuses one for the other, regardless of what the user typed.
What does not work is escaping input by hand. Every hand-rolled escaping routine has an edge case, and encoding tricks exist specifically to defeat them. If you find yourself writing a function to sanitise input for a query, stop — the parameterised version is shorter and correct.
You do not need to invent anything here, and you should not. The requirements are well established.
Better still, use a well-maintained authentication library or provider. This is a solved problem where the cost of a subtle mistake is very high, and it is one of the clearest cases for not writing it yourself. Our companion piece on passwords, passkeys and two-factor authentication covers the user-facing side.
Cross-site scripting occurs when user-supplied content is rendered as markup rather than as text, letting an attacker run script in another user's browser — which means reading their session, acting as them, or capturing what they type.
Modern frameworks escape output by default, which has removed most of this problem. It returns when someone deliberately opts out — inserting raw HTML, rendering a URL supplied by a user into a link, or building markup by string concatenation.
Where user-supplied HTML genuinely must be rendered, such as in a rich text editor, sanitise it server-side with a maintained library and a strict allow-list of tags and attributes. A block-list of dangerous tags is not equivalent, because the list of dangerous things keeps growing.
| Risk | What it looks like | The fix |
|---|---|---|
| Broken access control | changing an ID in the URL works | authorise every request, deny by default |
| SQL injection | input concatenated into a query | parameterised queries, always |
| Cross-site scripting | user content rendered as markup | escape output; sanitise with an allow-list |
| Exposed secrets | keys in the repository or the front-end | environment variables and a secrets manager |
| Vulnerable dependencies | a package with a known CVE | automated scanning and scheduled updates |
| Security misconfiguration | debug mode on in production | explicit environment-specific settings |
API keys, database passwords and signing keys must never be committed. Once a secret is in git history it is effectively public — it is in every clone, every fork and every backup, and removing it from the current file changes none of that.
Use environment variables or a secrets manager, add a scanner to your pipeline that blocks commits containing credential patterns, and treat any secret that has ever been committed as compromised. Rotate it, do not just delete the line.
Anything shipped to the browser is public, including values in a front-end bundle, in configuration objects, or in a client-side environment variable. A key needed by a browser application must be safe to publish; anything else has to be called from your server, with the secret staying there.
A typical application is mostly other people's code. That code carries its own vulnerabilities, and one in a widely used package is discovered and exploited at scale within days.
Three habits cover it. Run automated dependency scanning in your pipeline. Update on a schedule rather than only in a panic, because a stack that is three years out of date cannot be patched quickly when it matters. And treat adding a dependency as a decision — a package with one maintainer and no releases in two years is a risk you are accepting on behalf of your users.

A B2B application let customers download their invoices from a URL containing a sequential invoice number. The endpoint verified that the user was logged in. It never checked that the invoice belonged to them.
A customer noticed by accident after mistyping a digit and landing on another company's invoice, complete with contact details and amounts. They reported it responsibly, which was fortunate.
The fix was one line: filter the query by the current user's organisation as well as the invoice ID. The subsequent audit found the same omission on four other endpoints, all written by different people over two years. Nobody had been careless; there had simply been no convention that made the check unavoidable.
Security that depends on every developer remembering every rule fails eventually. Security that is built into the structure of the code holds up far better.

Authorise every request against the specific resource and deny by default. Parameterise queries. Escape output and sanitise HTML with an allow-list. Hash passwords with a slow algorithm and rate-limit logins. Keep secrets out of source control and out of the browser. Scan and update dependencies on a schedule.
None of this requires a security specialism. It requires treating a small number of rules as non-negotiable and building them into the structure of your application, so that following them is easier than not.

The teams who do this are not more paranoid than everyone else. They have simply made the secure path the path of least resistance, which is the only version of security that survives a deadline.
Tap a star to share what you thought.
No ratings yet
Broken access control — failing to verify that the logged-in user is permitted to act on the specific resource they requested. It regularly tops industry risk rankings and typically requires no tooling to exploit beyond editing a URL.
Use parameterised queries so the query structure and the user-supplied values are sent separately. Never build queries by concatenating strings, and do not write your own escaping routines — every hand-rolled version has an edge case.
Hash them with an algorithm designed to be slow: bcrypt, scrypt or Argon2. Fast general-purpose hashes such as SHA-256 are unsuitable precisely because their speed makes large-scale guessing cheap.
Sign in to join the conversation.
Loading responses…
Have a story, idea, or something valuable to share? Join The Blog Story for free, publish your content, reach more readers, and earn a share of advertising revenue from eligible content.
Create quality content. Grow your audience. Grow your earning potential.
Only if the key is safe to publish. Anything shipped to the browser can be read, including bundled configuration and client-side environment variables. Secrets must stay on your server, with the browser calling your endpoint instead.
Treat it as compromised and rotate it. Removing the line does not help, because the value remains in history, in every clone, fork and backup that was ever taken. Then add automated secret scanning to prevent a repeat.
Sanitise it on the server with a maintained library and a strict allow-list of permitted tags and attributes. Block-lists do not work, because the set of dangerous constructs keeps growing and you will always be behind it.
On a regular schedule, not only when a vulnerability is announced. A stack that is years out of date cannot be patched quickly under pressure, which is exactly when you will need to. Automated scanning in the pipeline makes this routine.
Generally no. It is a well-solved problem where subtle mistakes are costly and easy to make. A maintained library or identity provider gives you password hashing, session handling, rate limiting and multi-factor support that has been reviewed far more than yours will be.