Securing the Browser Layer and Your APIs — ASVS 5.0 V3 & V4
    Application Security

    Securing the Browser Layer and Your APIs — ASVS 5.0 V3 & V4

    CSP, CORS, DOM XSS, mass assignment, GraphQL depth limits — what OWASP ASVS 5.0 actually requires for web frontends and API security.

    Reverse PolarityFebruary 27, 20269 min read

    OWASP ASVS 5.0 organises its ~350 requirements into 17 chapters. Two of them deal directly with what happens at the HTTP boundary that users and API consumers actually touch: V3 (Web Frontend Security) and V4 (API and Web Service). This article walks through both chapters, what the requirements mean in practice, and how the two sets of controls interact when you're running a single-page application that talks to your own API.

    Diagram — Browser ↔ API Security Layers (ASVS 5.0 V3 & V4)
    Diagram

    V3 Web Frontend Security

    V3 is scoped explicitly to browser-facing applications. It doesn't apply to machine-to-machine scenarios. Its requirements sit in six sections covering content interpretation, cookies, security headers, origin separation, and external resource integrity.

    Content Security Policy

    CSP is the biggest lever in V3, and ASVS is precise about what each level demands.

    Requirement 3.4.3 (Level 2 baseline):

    HTTP responses must include a Content-Security-Policy header. At minimum a global policy must use object-src 'none' and base-uri 'none' and define either an allowlist or use nonces or hashes.

    Level 3 raises the bar further — a per-response policy using nonces or hashes is required, not just a single site-wide header. And 3.4.7 (Level 3) requires a report-uri or report-to directive so that violations are sent somewhere useful.

    The two mandatory directives are worth spelling out:

    • object-src 'none' prevents Flash and other plugin-embedded content from executing. Most modern apps don't use these anyway, but the directive closes the door explicitly.
    • base-uri 'none' prevents an attacker who can inject an HTML <base> tag from redirecting all relative URLs — a subtle but effective privilege escalation path.

    A minimal Level 2 header:

    Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'none'; script-src 'self'
    

    A Level 3 nonce-based policy (generated per response):

    Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'none';
      script-src 'nonce-r4nd0m123' 'strict-dynamic';
      report-uri /csp-violations
    

    'strict-dynamic' works alongside nonces so that trusted scripts can load further scripts without the CSP needing to enumerate every CDN URL.

    Clickjacking: frame-ancestors, not X-Frame-Options

    ASVS 3.4.6 (Level 2) requires using the frame-ancestors CSP directive to control framing and explicitly notes that X-Frame-Options is obsolete and must not be relied upon. The distinction matters: X-Frame-Options has no allowlist syntax that's consistent across browsers; frame-ancestors does.

    Content-Security-Policy: frame-ancestors 'none';          # disallow all framing
    Content-Security-Policy: frame-ancestors 'self';          # allow same-origin frames only
    Content-Security-Policy: frame-ancestors https://portal.example.com;   # specific allowlist
    

    Set frame-ancestors 'none' unless your application is explicitly embedded somewhere. If you need embedding, list the exact origins.

    Subresource Integrity

    Requirement 3.6.1 (Level 3): client-side assets loaded from external hosts — CDN-hosted JavaScript libraries, CSS, web fonts — must use Subresource Integrity (SRI) hashes, or there must be a documented security decision justifying why not.

    SRI ensures that even if a CDN is compromised, the browser refuses to execute the tampered file:

    <script
      src="https://cdn.example.com/lib/react.production.min.js"
      integrity="sha384-<base64-hash>"
      crossorigin="anonymous">
    </script>
    

    The crossorigin="anonymous" attribute is required for the integrity check to work because the browser needs to read the response body. Without it, the check is silently skipped on cross-origin resources in some browsers.

    SRI is only meaningful for static, versioned resources. It can't be applied to dynamically generated bundles unless you regenerate the hash at build time and inject it into your HTML — which most modern build toolchains support.

    DOM-Based XSS

    DOM XSS is listed under V3.2 (Unintended Content Interpretation), not in a dedicated XSS section, which reflects how ASVS 5.0 thinks about the problem. Reflected and stored XSS are primarily addressed by V1 (Encoding and Sanitization) because the injection happens in server-rendered output. DOM XSS is different: the payload never reaches the server, it's introduced by client-side JavaScript reading from an attacker-controlled source (URL fragment, document.referrer, postMessage, query parameters) and writing into a sink without sanitisation.

    Requirement 3.2.2 (Level 1):

    Content intended to be displayed as text must use safe rendering functions such as createTextNode or textContent to prevent unintended HTML execution.

    This is the practical fix for the most common DOM XSS pattern — code that does:

    // Vulnerable
    element.innerHTML = userInput;
    
    // Safe
    element.textContent = userInput;
    

    Requirement 3.2.3 (Level 3) addresses DOM clobbering — a related but distinct issue where an attacker's HTML (injected via innerHTML even when it doesn't contain <script>) creates DOM elements that shadow global JavaScript variables. The mitigation is explicit variable declarations, strict type checking, and namespace isolation.

    For postMessage-based DOM XSS, requirement 3.5.5 (Level 2) requires that messages whose origin is not in a trusted list are discarded before any processing.

    CORS

    CORS requirements appear in 3.4.2 (Level 1):

    The Access-Control-Allow-Origin header must be a fixed value, or if the request Origin header is used dynamically, it must be validated against an allowlist of trusted origins. When Access-Control-Allow-Origin: * is used, the response must not include sensitive information.

    The dangerous pattern this is prohibiting is the "origin reflection" anti-pattern:

    // What some frameworks do by default — dangerous
    res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    

    This effectively disables CORS as a protection mechanism. Any origin can make credentialed requests and read the response. An attacker's site can issue requests to your API on behalf of an authenticated user and receive the JSON payload.

    The correct approach is an explicit allowlist:

    const ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com'];
    
    const origin = req.headers.origin;
    if (ALLOWED_ORIGINS.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
    }
    

    Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is rejected by browsers. So wildcard ACAO is only viable for public, unauthenticated resources. ASVS 3.4.2 acknowledges this: if you use wildcard, the response genuinely must contain nothing sensitive.


    V4 API and Web Service

    V4 covers HTTP APIs: REST, GraphQL, WebSocket, and general service hygiene. It opens with a note that authentication, session management, and input validation from other chapters also apply — V4 doesn't stand alone.

    CSRF in the API Context

    Cross-site request forgery is addressed in V3.5 (Browser Origin Separation) rather than as a standalone chapter. ASVS 5.0 frames CSRF as one instance of the broader problem of verifying that a request originates from the application itself, not from a forged cross-origin page.

    Two complementary requirements cover the main approaches:

    3.5.1 (Level 1): If the application does not rely on CORS preflight to block cross-origin requests, it must use anti-forgery tokens or require non-CORS-safelisted custom headers.

    3.5.2 (Level 1): If the application does rely on CORS preflight as the protection, it must ensure requests cannot bypass preflight — by verifying Origin and Content-Type headers, or by requiring a custom header.

    The practical split depends on your cookie configuration. If your session cookie is SameSite=Strict or SameSite=Lax, browsers block it from being sent on cross-origin requests in most contexts, which breaks most CSRF attack vectors. ASVS 3.3.2 (Level 2) requires cookies' SameSite attributes be set appropriately. When that's in place and your API uses Content-Type: application/json for state-changing requests (triggering a CORS preflight), explicit CSRF tokens become largely redundant.

    But "largely redundant" is not "unnecessary." SameSite=Lax still sends the cookie on top-level navigations — a GET request triggered by a link. This is why 3.5.3 (Level 1) requires sensitive operations to use POST, PUT, PATCH, or DELETE, not safe HTTP methods. A side-channel that allows CSRF via a GET request is a distinct failure mode from cookie leakage.

    For applications that can't rely on SameSite (e.g., where the SPA and the API are on different subdomains and the session cookie needs to be shared), the explicit anti-forgery token pattern remains necessary.

    Mass Assignment

    Mass assignment is addressed in V15.3 (Defensive Coding) rather than V4 directly, but it's a prevalent API vulnerability and worth flagging here. The requirement:

    APIs must return only the required fields from data objects. Mass assignment must be prevented through allowlist-based binding.

    Mass assignment happens when an API endpoint passes user-supplied JSON directly into an ORM or object constructor without filtering which fields are writable:

    // Vulnerable — user can supply { "role": "admin", "credits": 999999 }
    const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
    
    // Safe — explicit allowlist of permitted fields
    const { name, email, bio } = req.body;
    const user = await User.findByIdAndUpdate(req.params.id, { name, email, bio }, { new: true });
    

    The ASVS requirement for allowlist-based binding applies both to input (what fields are accepted) and output (what fields are returned). Returning passwordHash, internalFlags, or stripeCustomerId by default because they're on the model is a separate but related problem — V15.3 requires returning only required fields.

    GraphQL Security

    V4.3 is a short but specific section. Two requirements, both at Level 2:

    4.3.1: A query allowlist, depth limiting, amount limiting, or query cost analysis must be in place to prevent DoS via expensive nested queries.

    4.3.2: GraphQL introspection must be disabled in production unless the API is intended for third-party consumption.

    GraphQL's schema expressiveness is also an attack surface. A deeply nested query can trigger O(n^k) database lookups:

    # A malicious query that traverses friends-of-friends-of-friends...
    {
      user(id: "1") {
        friends {
          friends {
            friends {
              friends { name }
            }
          }
        }
      }
    }
    

    Without depth limiting, this can exhaust database connections or compute resources on a single request. ASVS accepts multiple mitigation strategies because the right choice depends on the schema:

    • Depth limiting (e.g., max depth of 5) is simple but can be too restrictive for deeply nested legitimate queries
    • Query cost analysis assigns weights to fields and resolvers and rejects queries exceeding a total budget — more precise but requires instrumentation
    • Persisted queries / query allowlists only permit a set of pre-approved queries — the strictest control, but impractical for APIs exposed to arbitrary third-party clients

    On introspection: __schema and __type queries return the complete API schema, including every type, field, and argument. In production, this is a reconnaissance gift. Disabling introspection doesn't fix authorization bugs, but it adds friction. The ASVS carve-out for APIs "intended for third-party consumption" reflects that public APIs or developer portals with documented schemas have a legitimate reason to leave introspection on.


    Where V3 and V4 Interact

    A React or Vue SPA calling a REST or GraphQL backend is subject to requirements from both chapters simultaneously. The SPA's HTML delivery is a V3 concern; the API it calls is a V4 concern. Several requirements only make sense when you hold both in mind.

    CORS and CSRF together: The SPA's cross-origin API call requires a correct CORS policy on the API server (V3/4.2). The CORS preflight serves double duty — it also functions as CSRF protection for non-simple requests (V3.5.2). Misconfiguring CORS doesn't just break legitimate functionality; it can neutralise the CSRF protection you're relying on.

    Cookie attributes are end-to-end: The SameSite, Secure, HttpOnly, and __Host- prefix requirements in V3.3 are set by the API's Set-Cookie response. They determine whether the SPA's requests are protected against CSRF and whether session tokens can be accessed by client-side JavaScript. Getting these wrong at the API layer undermines the browser-layer protections V3 is trying to enforce.

    Content-Type matters for preflight triggering: A SPA calling an API with Content-Type: application/json triggers a CORS preflight. If a developer changes the content type to a CORS-safelisted type (application/x-www-form-urlencoded, text/plain) to avoid preflight errors during development, they may inadvertently remove the preflight-based CSRF protection (V3.5.2).

    CSP and API responses: If the API returns data that the SPA injects into the DOM, V3.2.2's requirement to use textContent rather than innerHTML applies to the client-side rendering code, not the API. But if the API's responses contain HTML that the application renders — a rich-text field, a CMS excerpt — both V1 (server-side sanitisation) and V3 (safe rendering functions or CSP) apply together.

    HTTP method discipline (V3.5.3): The requirement that sensitive operations use POST, PUT, PATCH, or DELETE originates in the browser origin separation section of V3 but shapes the API's routing design. A REST API that handles state-changing operations over GET (even if it seems harmless) opens a CSRF surface that SameSite=Lax doesn't fully close.


    Quick Reference: Level Requirements

    Requirement V3/V4 L1 L2 L3
    CORS allowlist validation 3.4.2 X X X
    CSP global policy (object-src, base-uri) 3.4.3 X X
    CSP per-response with nonces/hashes 3.4.3 X
    CSP violation reporting endpoint 3.4.7 X
    frame-ancestors directive 3.4.6 X X
    X-Content-Type-Options: nosniff 3.4.4 X X
    Referrer-Policy header 3.4.5 X X
    SameSite cookie attribute set 3.3.2 X X
    HttpOnly on session cookies 3.3.4 X X
    Anti-CSRF tokens or preflight reliance 3.5.1/2 X X X
    SRI for external assets 3.6.1 X
    GraphQL depth/cost limiting 4.3.1 X X
    GraphQL introspection disabled 4.3.2 X X
    WebSocket uses WSS 4.4.1 X X X
    Intermediary headers not user-overridable 4.1.3 X X

    The V3 and V4 requirements don't describe optional hardening — they describe the baseline expected of a Level 2 application, which is ASVS's definition of standard, comprehensive security practice. Most of the controls here — CSP, SRI, SameSite, proper CORS configuration, GraphQL depth limiting — have been implementable for years. The gap is usually not capability but deliberate configuration.

    More Articles