Cybersecurity Engineering Handbook / Chapter 48
Web Application Security Playbook
Secure web applications by mapping browser-facing risks to controls, implementation rules, and tests.
Preparing audio…
Audio edition
Web Application Security Playbook
An invoice application adds a preview button. A user supplies Markdown, the server fetches remote images, and the browser renders the result beside uploaded attachments. The feature looks small. Its request crosses a session boundary, a tenant boundary, a parser, an outbound network client, object storage, and several browser interpreters. A defect in any one of them can turn preview into another tenant’s document, stored script execution, access to an internal service, or a public file.
The useful unit of web security is therefore not the route. It is the complete request path: authenticate the actor, authorize the object and action, constrain every interpretation of untrusted data, bound every side effect, and preserve evidence of the decision. Follow one real path through those gates and omissions become visible.
Begin with the request the server will accept
Suppose the preview request arrives with a session cookie, document id, tenant id, Markdown body, remote-image option, and attachment references. None of those fields proves authority. The browser can alter hidden inputs, replay old requests, call endpoints directly, and present object identifiers learned elsewhere.
For a browser session, keep the credential out of script-readable storage when the architecture permits it. Set the session cookie with Secure and HttpOnly; choose SameSite from the application’s actual cross-site flows; omit Domain unless subdomains genuinely need the cookie; narrow Path; and give the session a lifetime appropriate to its authority. Rotate the session after login, account recovery, privilege elevation, and other changes that could otherwise preserve an attacker-chosen or lower-assurance session. Logout, password reset, administrative revocation, and account disablement need defined server-side invalidation behavior, not merely deletion of a browser cookie.
Cookie flags limit exposure; they do not decide whether a request is allowed. The server resolves the authenticated actor, loads the document under the authoritative tenant scope, and asks whether that actor may preview this document in its current state. Tenant ids, roles, prices, feature entitlements, and ownership claims supplied by the client are hints at most. The policy decision comes from trusted server-side state.
Account recovery, MFA reset, email change, support impersonation, and administrative elevation deserve reauthentication, short-lived authority, rate limits, and high-quality audit events. They change who can exercise authority, so a generic “profile updated” log cannot support investigation.
Make object authority survive every path
A route-level check that establishes “this user is logged in” says nothing about document 123. The preview handler must authorize the action against the resolved document, its tenant, the actor’s relationship to it, and relevant state such as archived or locked. The attachment reader and remote-rendering job must carry the same authority boundary; moving work to a queue does not make it trusted.
Policy scattered across controller conditionals will drift. Give handlers a shared, testable decision point, but keep object selection policy-aware too. Loading by global id and checking afterward can create timing, logging, cache, or error differences; bulk queries, search, exports, and caches are safer when tenant and authorization constraints shape the lookup itself.
The decisive tests are negative. Change only the document id to one owned by another tenant. Ask a viewer to perform an editor action. Request an archived object, a soft-deleted attachment, an export containing a mixture of authorized and unauthorized rows, and a cached response after access is revoked. Run the same cases through browser requests, background jobs, and direct object-storage retrieval. A denial at the page route is not enough if a secondary path still returns the bytes.
Treat each interpreter as a new boundary
“Validated input” is not a transferable safety property. A string acceptable as Markdown may be dangerous as HTML; a safe URL label may be unsafe as a fetch destination; a filename may be unsafe as a storage key or response header. Validate structure and business rules when data enters the application, then encode or constrain it again for the interpreter that consumes it.
For the preview body, bound length and nesting before parsing. If the product needs plain text with formatting, render through a maintained Markdown implementation configured to reject raw HTML. If it genuinely needs authored HTML, sanitize with an explicit element, attribute, and URL-scheme policy. Do not sanitize and then concatenate new markup or pass the result through a transformation that can reintroduce executable content.
At the browser boundary, prefer text nodes and framework bindings that escape by default. Encode untrusted values for the exact destination: HTML text, a quoted safe attribute, a URL component, JSON, CSS, or JavaScript are different grammars. Avoid putting untrusted data into script blocks, event-handler attributes, style rules, tag names, or other dangerous contexts. Review every framework escape hatch such as raw-HTML rendering and every DOM sink such as innerHTML; their names differ, but their consequence is the same.
Parameterized database operations remain the default. Review the places that evade them: raw-query helpers, dynamic column or sort selection, search expressions, report builders, template engines, shell commands, and unsafe deserialization. When user-defined behavior is a real product requirement, expose a constrained language and a resource-bounded execution boundary rather than a general evaluator.
An XSS prevention review for the preview path should answer four concrete questions:
- Which values can an attacker influence, including stored records, URL fragments, third-party responses, and browser messages?
- Into which parser context does each value flow, and which encoder or sanitizer is responsible at that final boundary?
- Where does the framework’s automatic escaping stop, and which raw-HTML or DOM sinks remain?
- Which regression payloads prove that HTML, attribute, URL, script-like, and stored-content cases render as data rather than execute?
Bound the side effects behind the page
Preview need not save a document to cause harm. Its remote-image option makes the server perform a network action with more reach than the browser may have. The safest design removes arbitrary fetching: proxy images only from business-approved destinations, or require uploads. When arbitrary public destinations are essential, isolate the fetcher, allow only required schemes and ports, reject credentials in URLs, resolve and validate all returned addresses, block loopback, link-local, private, metadata, and other non-public ranges for both IPv4 and IPv6, and apply the decision again after resolution. Disable redirects or validate every hop. Bound connection time, total time, response bytes, decompression, content types, and concurrent work. Egress policy and monitoring should still assume application validation can fail.
The SSRF review is incomplete until tests attempt alternate address forms, IPv6, DNS changes, redirects to blocked ranges, non-HTTP schemes, oversized and compressed responses, slow responses, and a destination that becomes unavailable. Record the normalized destination, resolved address, decision, bytes, duration, and block reason without logging embedded credentials. If the product cannot state which destinations the feature must reach, it cannot yet state a defensible network policy.
Uploads cross a different boundary. Enforce size and count before expensive parsing, inspect actual content rather than trusting the browser’s media type or extension, rename objects, store them outside executable or public paths, and serve them with an intentional content type and disposition. Malware scanning may be appropriate, but a clean scan does not make an active document safe. Retrieval still requires authorization, and image or document processing belongs in a resource-limited worker because parsers themselves are attack surfaces.
State-changing requests authenticated by cookies also need a CSRF design. Use a framework-supported anti-CSRF token pattern and verify it on the server; SameSite cookies add protection but must match legitimate cross-site and embedded flows. Verify request origin where it is a sound additional signal. GET, HEAD, and other safe-method handlers must not hide state changes. CORS controls which browser origins may read or send certain cross-origin requests; it is not object authorization and does not stop ordinary cross-site form submission by itself.
Give the browser an explicit policy
Security headers are a deployable policy, not a copied block of fashionable directives. Begin with an HTTPS-only application and set Strict-Transport-Security only after confirming that the named host—and any subdomains covered with includeSubDomains—can remain HTTPS-only for the chosen lifetime. Send X-Content-Type-Options: nosniff. Use Referrer-Policy to limit URL data disclosed across navigations, a narrow Permissions-Policy for browser capabilities the application does not need, and explicit cache controls on sensitive responses.
Build Content Security Policy from observed resource needs. Prefer nonces or hashes for scripts and a restrictive source policy over broad host lists and 'unsafe-inline'. Set object-src 'none' when plugins are unnecessary and use frame-ancestors to name who may embed the page; it does not inherit from default-src. Roll out a changed policy in report-only mode where breakage risk warrants observation, inspect violations, then enforce it. CSP reduces the blast radius of some injection defects but does not replace context-correct rendering.
Test the baseline on authenticated, unauthenticated, error, redirect, download, preview, legacy, and direct-origin responses. A rule applied only at one edge can disappear on a preview host or bypass route. Treat deliberate exceptions as named policy: owner, affected route, reason, compensating control, and removal date.
Browser state needs the same precision. Decide which origins may call the application, which may embed it, and which postMessage origins and message shapes are accepted. Service-worker scope, local storage, browser caches, and back-forward cache can preserve data after logout unless the design accounts for them. Third-party frames should receive only the sandbox capabilities they need.
Account for code that arrives from elsewhere
A third-party script runs inside the application’s trust boundary unless isolation says otherwise. Analytics, support widgets, tag managers, A/B testing code, and compromised dependencies may read page content and form values even when they cannot read an HttpOnly cookie. Inventory such scripts by owner and purpose. Remove abandoned packages, pin deterministic resolutions, review lockfile and build-script changes, and separate untrusted content from trusted build steps.
Prefer self-hosting reviewed assets. Subresource integrity can bind a fixed cross-origin script or style to expected bytes, but it does not help code whose provider is expected to change on every request. CSP can restrict sources, yet allowing a third party still grants that source substantial power. Sensitive workflows may deserve a separate origin or page that does not load optional third-party code at all.
Preserve enough evidence to explain a denial
The user-facing error should reveal no stack trace, secret, internal address, query, or cross-tenant object existence. Internally, the preview event needs a request id, actor, tenant, target object, action, policy result, validation-failure family, fetch or upload decision, response class, and relevant risk signals. Prefer stable identifiers and bounded categories over raw request bodies. Session credentials, reset tokens, document contents, and unnecessary personal data do not become safe merely because they entered a security log.
Alerting follows decisions that may indicate abuse: repeated cross-object denials, credential stuffing, recovery attempts, bursts of sanitizer rejection, blocked internal fetches, upload scanner findings, unusual exports, and administrative actions outside established patterns. Monitor the health of the logging and alert path itself. A quiet detector is evidence only when collection, parsing, routing, and notification are known to work.
Review one path from browser to consequence
Do not close the review with eight independent green boxes. Choose a consequential request and follow it through the deployed system. For preview, the review record should identify:
- the actor and session assurance, including rotation, expiry, revocation, and CSRF behavior;
- the object lookup and policy decision for read, preview, edit, attachment retrieval, and queued work;
- every untrusted value and the parser, query, template, DOM sink, header, file path, or network client that consumes it;
- outbound destinations and upload behavior, including redirect, resolution, size, time, storage, and retrieval boundaries;
- the enforced cookie, CSP, framing, referrer, permissions, content-type, and cache policy on every response path;
- frontend and third-party code allowed to execute in the page;
- safe user errors, protected logs, alert routes, and telemetry-health evidence;
- the owner of each exception and the test that prevents it from becoming permanent.
Attach configuration or code references and test results to the record. “Uses framework defaults” is acceptable evidence only when the review identifies the default, its versioned configuration, and the paths that bypass it.
Tests that make the argument credible
A useful suite varies one boundary at a time and then combines boundaries attackers can chain.
- Replay an expired and a revoked session; rotate privilege while an old session remains open; submit a state change without the CSRF proof.
- Substitute another tenant’s document and attachment ids through the page, direct endpoint, bulk operation, background job, and cache.
- Render hostile stored content into HTML text, attributes, links, client templates, and any permitted rich-text path; verify execution does not occur and CSP reports are handled without becoming the primary defense.
- Send malformed, deeply nested, oversized, and unexpected fields through every parser and raw-query escape hatch.
- Exercise the remote fetcher with disallowed schemes, alternate address representations, private and link-local IPv4 and IPv6 destinations, DNS changes, redirects, slow servers, decompression expansion, and oversized bodies.
- Upload active, mismatched, oversized, duplicate, and scanner-failing files; retrieve them with the wrong actor and through any public storage URL.
- Inspect headers and cache behavior on success, denial, error, redirect, download, preview, legacy, and direct-origin responses.
- Trigger a representative authorization denial, blocked fetch, and recovery abuse pattern; prove the event is redacted, reaches the detector, and produces an actionable notification.
The tests are strongest when they preserve the exploit shape of a real defect. A fix for stored script execution should leave behind the payload, rendering context, expected inert output, and policy signal that would reveal recurrence.
Field reference
For each sensitive web request, write a one-line security claim: this actor may perform this action on this object, using this bounded input, producing these browser and server effects. Then locate the evidence for each phrase.
- Actor: session origin, assurance, lifetime, rotation, revocation, reauthentication, and CSRF proof.
- Action and object: authoritative tenant-scoped lookup, shared policy decision, indirect paths, and negative access tests.
- Bounded input: structural and business validation followed by the correct constraint at every parser or interpreter.
- Browser effects: context-correct encoding or explicit sanitization, enforced CSP and framing policy, safe cookies, and controlled third-party code.
- Server effects: parameterized queries, constrained outbound requests, isolated uploads and parsers, limits, and least-privilege execution.
- Evidence: safe errors, redacted decision logs, abuse alerts, telemetry-health checks, and regression tests that retain the attack shape.
If one phrase has no owner, enforcement point, or failing test, the request path is not yet secure enough to approve.
Continue reading
Full table of contents