Security

Web Security: The Defences That Earn Their Keep

Most breaches are not clever. They are a missing check on a route nobody remembered. A practical tour of the OWASP risks that actually reach production, and the layered defences that stop them.

Concentric shield layers protecting an application core from incoming attacks

The short version

  • Broken access control is the most common serious flaw in real applications, and it is almost never a framework bug. It is a check nobody wrote.
  • Defence works in layers. No single control is sufficient, and every control assumes the ones around it will sometimes fail.
  • Dependencies are now a bigger attack surface than your own code. Treat your lockfile as production infrastructure.
  • If you only do four things: enforce authorisation server-side on every route, parameterise every query, patch dependencies automatically, and log enough to reconstruct an incident.

Most breaches are not clever. The mental image people carry — a determined attacker finding a novel flaw in your cryptography — is almost never what happens. What happens is that an endpoint added during a rushed sprint checks that you are logged in but forgets to check which account you are allowed to see, and eight months later someone increments an ID in a URL.

That gap between the security people imagine and the security incidents that actually occur is the reason this article exists. What follows is a practical tour of the risks that reach production in real applications, and the defences that are worth the effort of maintaining.

Start with the risks that actually occur

The OWASP Top 10 is the closest thing the industry has to a shared list of what goes wrong. It is compiled from real application data rather than theory, which is exactly what makes it useful as a starting point.

Broken access control

This sits at the top of the list because it is both the most common and among the most damaging. The pattern is nearly always the same: authentication is handled correctly and centrally, and authorisation is handled ad hoc, per endpoint, by whoever wrote that endpoint.

The specific failures worth auditing for:

  • Insecure direct object references. A URL contains a record ID, the handler loads the record by ID, and nothing confirms the current user owns it.
  • Client-side enforcement. The button is hidden for non-admins, but the endpoint behind it is not protected. Hiding a control is presentation, not security.
  • Missing function-level checks. The list endpoint checks permissions; the export endpoint, added later, does not.
  • Mass assignment. A form update accepts whatever fields arrive, including role or account_id.

The structural fix is to make authorisation impossible to forget. Deny by default at the framework level, so an unannotated route fails closed. Scope every query to the current tenant or user at the data-access layer rather than in each controller, so the unsafe version requires deliberate effort to write.

Injection

SQL injection has been well understood for over two decades and still appears, because the unsafe version is the one that is easier to type when you are in a hurry. Parameterised queries and a competent ORM solve the common case entirely. The residual risk lives in the places people reach around the ORM: raw query fragments, dynamic ORDER BY clauses built from user input, and search filters assembled by string concatenation.

The same class of problem recurs elsewhere. Command injection when user input reaches a shell. Template injection when input is rendered as a template rather than as data. Anywhere a string crosses from data into instructions, the boundary needs an explicit, encoding-aware conversion.

Cross-site scripting

Modern frameworks escape output by default, which has dramatically reduced XSS — and concentrated the remaining risk in the escape hatches. React’s dangerouslySetInnerHTML, Rails’ html_safe, and their equivalents are named to sound alarming for good reason. Every use is a decision that needs justifying.

The layer beneath is Content Security Policy. A strict CSP means that even successfully injected script has nowhere to execute from and nowhere to send stolen data. It is genuinely fiddly to introduce into an existing application, and it is the single control that most reliably converts an XSS finding from critical into cosmetic.

Vulnerable and outdated components

This is where the balance of risk has shifted most in recent years. A typical application ships far more third-party code than first-party code, and an attacker looking for a way in will check your dependency versions long before they look at your logic.

  • Enable automated dependency updates and actually merge them. A bot that opens pull requests nobody reviews is worse than nothing, because it manufactures the appearance of diligence.
  • Commit lockfiles and build from them, so what you tested is what you deploy.
  • Run a vulnerability scanner in CI and fail the build on high-severity findings in production dependencies.
  • Periodically audit what you actually use. The safest dependency is the one you removed.

Authentication and session handling

Do not build authentication yourself. Use the framework’s implementation or a reputable provider, both of which have absorbed years of attacks you have not thought of.

The parts that remain yours regardless of what you adopt:

  • Password storage using a memory-hard function — Argon2 or bcrypt with an appropriate cost. Never a general-purpose hash, never a home-made scheme.
  • Rate limiting on login, password reset and any endpoint that reveals whether an account exists.
  • Multi-factor authentication, at minimum for administrative accounts. Compromised credentials are the entry point for a very large share of incidents.
  • Session hygiene: rotate the session identifier on privilege change, set HttpOnly, Secure and SameSite on cookies, and expire idle sessions.
  • Password reset flows, which are frequently the weakest authentication path in an application precisely because they receive the least design attention.

Watch the reset flow specifically. A password reset that emails a token which does not expire, can be reused, or is generated from a predictable source is a complete authentication bypass wearing a helpful user interface. It deserves the same review attention as the login endpoint.

Defence in depth

The reason to talk about layers rather than controls is that every individual control eventually fails. Layering means designing so that a single failure is contained rather than total.

In practice, for a typical web application:

  • Edge. TLS everywhere with HSTS, a WAF or managed rules for obvious automated traffic, and rate limiting before requests reach your application at all.
  • Application. Input validation at the boundary, authorisation on every route, output encoding by default, CSRF protection on state-changing requests, and security headers including CSP.
  • Data. Least-privilege database credentials, encryption at rest, and field-level encryption for the handful of columns that would be genuinely damaging to lose.
  • Infrastructure. Network segmentation so a compromised web tier cannot reach everything, secrets held in a secret manager rather than environment files in a repository, and immutable deploys.
  • Observability. Enough audit logging to reconstruct who did what, alerting on authentication anomalies, and a written incident plan.
Security is not a feature you finish. It is a property you maintain, and it decays quietly whenever nobody is paying attention.

Where to start with limited time

Nobody gets to do all of this at once. If you have a week rather than a quarter, the highest-yield sequence we have found is:

  1. Audit authorisation on every route. Enumerate your endpoints and confirm each one checks not just that a user is authenticated but that this specific user may act on this specific resource. This finds more real problems than everything else combined.
  2. Turn on automated dependency updates and clear the existing backlog of high-severity advisories.
  3. Add security headers, starting with HSTS and a report-only CSP so you can see what would break before enforcing it.
  4. Enforce MFA on administrative accounts, which is a configuration change rather than a project.
  5. Confirm you could investigate an incident. If someone reported a breach today, could you determine what was accessed and when? If not, that is the gap to close next.

Boolean Solutions experience with security

We build security into applications from the start and run reviews on applications that were built without it, which are two very different exercises. The reviews almost always surface the same two findings: authorisation applied inconsistently across endpoints, and a dependency tree nobody has looked at in over a year.

Neither is glamorous and both are entirely fixable. If you want a review before a customer’s security questionnaire forces one, talk to us.

Further reading

Written by

Udit Mittal

Founder at Boolean Solutions. Twenty years of building and rescuing web, mobile and AI products for SaaS companies and startups — and writing down what actually worked.

Get in touch