Reducing web application attack surface

Defence starts with reducing what is exploitable. Every control below addresses a class of vulnerability that appears consistently in web application testing. None of them is sufficient alone; together they raise the cost of exploitation significantly.

Input handling

Parameterised queries (prepared statements) eliminate SQL injection for database calls. This is not an input validation approach: it separates the query structure from the data at the driver level, so no input value can change the query’s meaning regardless of its content.

Auto-escaping engines are the safer choice for server-side template rendering; avoid passing user input directly into render(), eval(), or template string construction. If a feature genuinely requires dynamic template content, constrain what is dynamic rather than sanitising the input.

Validate file and URL inputs fed to outbound HTTP clients (image loaders, webhook handlers, document importers) against an explicit allowlist of permitted schemes, hosts, and ports. Block loopback addresses and link-local ranges (169.254.0.0/16, ::1) at the validation layer, not as an afterthought.

Configure XML parsers to disable external entity processing and DTD loading explicitly. The safe configuration is not the default in most libraries:

# Python lxml: disable external entities
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True)
// Java SAXParser
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

Output and response handling

Keep stack traces and detailed error messages from the client in production. A generic message with a correlation ID that maps to the full detail in server-side logs is the appropriate response. An error page that reveals the ORM type, database schema, or framework version is a discovery aid.

Include these security headers on every response:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin

The Content-Security-Policy is the most impactful XSS mitigation. A policy that disallows inline scripts and external script sources forces XSS payloads to find a script source the policy permits, which is significantly harder.

Authentication and session hardening

Setting session cookies with HttpOnly, Secure, and SameSite=Strict (or at minimum SameSite=Lax) limits the impact of XSS and CSRF. HttpOnly prevents JavaScript from reading the cookie. Secure prevents transmission over HTTP. SameSite=Strict prevents the cookie from being sent with cross-origin requests, eliminating most CSRF attack scenarios.

Restrict JWT implementations to an explicit algorithm allowlist rather than accepting whatever the token header declares. Reject the none algorithm unconditionally. Asymmetric algorithms (RS256, ES256) are preferable to symmetric ones (HS256) for tokens validated across multiple services. Key rotation belongs in the production runbook, not the backlog.

Short expiry times on password reset tokens and one-time codes (fifteen minutes is a common ceiling), binding to the issuing email address, and invalidation after first use are the relevant controls.

Access control

Enforce access control server-side on every request, not assumed from the UI flow. A deny-by-default approach, where every endpoint requires authentication unless explicitly marked public, prevents the common pattern of an endpoint that was “internal” during development and accidentally left open.

Prevent mass assignment with explicit allowlists at the model or serialiser layer, not by stripping fields from user input. A field with no business reason for client modification is not assignable regardless of what the client sends.

Separate privileged operations from the main application surface. Admin functionality served from a separate subdomain or path prefix that is only accessible from specific network sources is harder to test and harder to abuse.

Business logic

Atomic database transactions or application-level locks are the right control for shared-state operations. A check-and-write that reads a value and then updates it without holding a lock is exploitable with concurrent requests. Idempotency keys on financial operations prevent double-spend from race conditions and duplicate submissions.

Validate workflow state transitions server-side at every step, not only at the start and end. If a refund is only valid when an order is in “delivered” status, that status check goes at the refund endpoint, not implied by the UI flow that precedes it.