Server-Side Security: Authorisation Is the Weak Point
Injection grabs attention, and authorisation causes breaches. Broken access control has been the top web application risk for four consecutive editions, and it's found in essentially every application tested.
Most server-side security advice is a checklist: patch your software, configure your firewall, validate your input, audit regularly. Everything on it is worth doing, and the ordering tells you the author started from a list rather than from the data.
The data says something more specific. Broken access control has been the top application security risk for four consecutive editions of the OWASP Top 10, and in the 2025 edition every single application in the contributed dataset showed some form of it. Not most. All of them.
Meanwhile, injection, the vulnerability class that dominates the discourse, has fallen to fifth.
So this starts where the failures actually are.
What the server owes
One framing point first, because it decides everything else.
The server is the only place enforcement can live. Client code is downloadable, readable, and modifiable, whether it's JavaScript in a browser or a compiled mobile binary. Every check that matters must exist server-side, regardless of what the client does, which is the argument I made from the other direction in the piece on front-end security.
That's not a shared responsibility. It's entirely the server's job.
Broken access control
Authentication asks who you are. Authorisation asks what you're allowed to do. Systems get the first right and the second wrong, all the time.
The insecure direct object reference is the canonical version:
GET /api/invoices/4471
The endpoint checks you're logged in, fetches invoice 4471, and returns it. It never checks the invoice belongs to you. Change the number, and you're reading someone else's finances. No injection, no exploit, just a missing line.
Missing function-level checks are the same failure on actions rather than data. The admin button is hidden from ordinary users, and the endpoint behind it isn't protected, so anyone who finds the URL can call it. Hiding a control is a user experience decision, not a security one.
Mass assignment is the version people miss. A profile update endpoint binds the request body straight onto the model, and someone adds "role": "admin" to the JSON. Bind explicitly to a permitted set of fields, never to whatever arrived.
Three things actually help.
Deny by default: Access should require an explicit grant, not the absence of a denial. A new endpoint added without an authorisation decision should fail closed, not open.
Enforce at the data layer, not the controller: Scope every query to the current user rather than fetching first and checking after:
SELECT * FROM invoices WHERE id = ? AND organisation_id = ?
Controller checks depend on someone remembering. A scoped query is right by construction, and it follows the same principle as putting constraints in the schema rather than trusting application validation, which I covered in the database management piece.
Test authorisation-like functionality: Write the test where user A requests user B's resource and expects a 404. Almost nobody does this, and it's the single most valuable test suite you can add.
Worth noting that the 2025 revision folded SSRF into this category, on the reasoning that a forged server-side request is the same failure one layer down: something acting without a check on what it's permitted to reach. If your application fetches a URL supplied by a user, allowlist the destinations. An attacker supplying a cloud metadata endpoint can retrieve your instance credentials, which turns a minor feature into a total compromise.
Injection, and the distinction that matters
Injection dropped in the rankings because the fix is well understood and widely applied. It hasn't disappeared, and the fix is worth stating precisely, because the original advice on this topic is usually wrong in a specific way.
"Validate and sanitise input" conflates two different operations.
Validation rejects input that doesn't match what you expect: an email that isn't an email, a negative quantity. It's about business rules, and it belongs at the boundary.
Safe handling at the point of use prevents injection, and it depends on where the data is going. For SQL, that's parameterised queries:
SELECT * FROM users WHERE email = ?
The value is never parsed as SQL, so it cannot become SQL, regardless of what it contains. No amount of validation gives you that guarantee, and blocklists of dangerous characters have failed every time they've been tried.
Sanitisation, meaning stripping dangerous constructs, is a concept from HTML output. It's the wrong tool for SQL entirely.
Watch for the gaps your ORM leaves. Raw SQL fragments, dynamic column or table names, and ORDER BY clauses built from user input are all places where parameterisation doesn't apply, and those need an allowlist of permitted values instead.
The same principle covers command injection. Don't build shell strings; use the process API that takes an argument array, so nothing is ever parsed by a shell.
Configuration
Security misconfiguration rose from fifth to second in 2025, which reflects how much surface modern deployments have.
The recurring offenders: default credentials left in place, debug mode enabled in production, verbose error pages that return stack traces to users, directory listing enabled, admin interfaces reachable from the internet, cloud storage buckets set to public, and permissive CORS headers on authenticated endpoints.
Two worth expanding.
Error responses. A stack trace tells an attacker your framework, your versions, your file paths, and sometimes your query structure. Log the detail, return a generic message and a reference ID.
CORS is not access control. It governs what other origins' JavaScript may read in a browser. Access-Control-Allow-Origin: * on a public API is fine; the same header with credentials on an authenticated endpoint is a problem. Either way, it protects nothing on the server side.
The supply chain
New in the 2025 list at number three, elevated from a components footnote, and for good reason: most of a modern codebase is code you didn't write.
The server-side measures are the same ones I set out for npm in the front-end security piece, and they apply to every ecosystem. Commit lockfiles and install with the deterministic command in CI. Disable install scripts by default. Delay adoption of new releases, since malicious versions are usually caught within hours. Reduce the dependency count, because each one is a maintainer account that can be phished.
Then automate the watching, with Dependabot or Renovate for updates and something behavioural for packages that suddenly start making network requests, which a CVE database won't tell you about.
Authentication
Password storage has one answer: Argon2id, or bcrypt if your platform gives you that. Never a general-purpose hash like SHA-256, with or without a salt, because those are designed to be fast and speed is exactly what you don't want.
Rate-limit authentication endpoints by account and by source. Credential stuffing uses valid credentials from other breaches, so it doesn't look like brute force. Detecting many accounts failing from one source, or one account attempted from many sources, catches it.
Tokens: Server-side sessions are simpler and easier to revoke. JSON Web Tokens are convenient and hard to revoke, which is their main practical drawback: a stolen token stays valid until it expires. If you use them, keep access tokens short-lived, rotate refresh tokens on every use, and pin the algorithm rather than trusting the header. Long-lived sessions on mobile need their own handling, which I covered in the mobile backends piece.
Secrets
Not in the repository. Not in a committed environment file. Not in the container image.
Use whatever your platform provides, since it's always better than the alternative, and rotate on a schedule rather than only after an incident.
When a secret leaks, rotating it is the fix, and deleting the commit is not, because git history persists in forks, clones, and caches. Assume anything committed is public the moment it's pushed.
File uploads
The original advice here was broadly right and worth making precise.
Verify content, not the extension or the declared MIME type, both of which the client controls. Store uploads outside the web root, or better, in object storage. Serve them from a different domain so a malicious file can't execute in your application's origin. Generate your own filenames rather than trusting the supplied one, which is how path traversal gets in. And enforce a size limit, since unbounded uploads are a denial of service with no exploit required.
Logging, and what never to log
Log authentication attempts, authorisation failures, administrative actions, and changes to permissions. Repeated authorisation failures from one account are one of the clearest signals of someone probing your endpoints.
Never log passwords, tokens, session identifiers, card numbers, or personal data. Logs are copied, shipped to third-party services, and read by people who don't need that access, and a secret in a log file has a much wider audience than a secret in a database.
Make sure someone actually looks. Logs nobody reads are a forensic record, not a defence.
What isn't yours
Worth separating honestly, because the original list mixed these in.
Web application firewalls, DDoS mitigation, network segmentation, and operating system patching are infrastructure concerns. They matter, and they're usually somebody else's job, and none of them fixes an authorisation bug in your code. A WAF in front of an IDOR is a WAF watching a legitimate-looking request go past.
Backups belong here for one reason the original didn't state: ransomware. An offline or immutable copy your production credentials cannot reach is what makes the difference between an incident and a catastrophe.
Where to start
Audit authorisation on your most sensitive endpoints: Take an object ID you own, change it to one you don't, and see what comes back. Do it for every resource type. This takes an afternoon and finds more than any scanner.
Confirm every query is parameterised, then grep for string concatenation near SQL.
Check what your error responses expose in production.
Get your dependency pipeline automated, with lockfiles, deterministic installs, and update alerts.
Then add the authorisation tests, so what you fixed stays fixed.
The short version
Authorisation is where applications actually fail, and it fails silently, because a missing check produces a working feature.
Deny by default, scope queries to the current user rather than checking after the fetch, bind request bodies to permitted fields explicitly, and test that user A cannot reach user B's data.
Parameterise everything, and understand that validation and safe handling are different jobs.
Treat dependencies as code you chose to run with full privileges, keep secrets out of the repository, and configure production as though an attacker will read errors.
And remember what the ranking is telling you: the interesting exploit gets written about, and the missing AND organisation_id = ? is what actually loses the data.