SecOps

Preventing CSRF Attacks: Enforcing Strict SameSite Cookies

By DexNox Dev Team Published May 20, 2026

Cross-Site Request Forgery (CSRF) remains a prominent vulnerability in web applications that rely on ambient authority mechanisms. Ambient authority refers to the behavior where web browsers automatically attach credential state—such as cookies, HTTP Basic Authentication headers, and SSL client certificates—to outbound HTTP requests, regardless of the origin initiating the request. If an authenticated user visits a malicious third-party site, that site can exploit this ambient authority by forcing the user’s browser to execute state-changing actions (POST, PUT, DELETE) on the target application without the user’s consent or knowledge.

To mitigate this attack vector, modern security architectures deploy a multi-layered defense strategy. This guide examines the design, performance profile, and implementation details of combining the SameSite cookie attribute with the stateless Double Submit Cookie pattern. By coupling these mechanisms, engineering teams build resilient systems that prevent credential abuse even when client-side configurations or browser versions vary.

Cryptographic Validation Mechanics

The mitigation strategy relies on two distinct layers: native browser policy enforcement via SameSite flags and application-level cryptographic verification using the Double Submit Cookie pattern.

The SameSite attribute controls whether cookies are sent with cross-site requests. This attribute supports three distinct configurations:

  • Strict: The browser prevents the cookie from being sent in any cross-site request context. This includes simple link navigation (clicking an anchor tag from a search engine to the target application).
  • Lax: The browser blocks cookies on cross-site subrequests (such as image loads or AJAX calls) but allows them when a user navigates to the origin site via a top-level GET request (e.g., clicking a standard link).
  • None: The browser sends cookies in all cross-site contexts, provided the Secure flag is active (requiring HTTPS).

While SameSite=Lax or SameSite=Strict offers strong default protections, they do not constitute a complete CSRF defense on their own. Legacy browsers lack support for these attributes, subdomains under the same public suffix can sometimes bypass domain barriers, and state-changing GET endpoints (which violate HTTP specifications but occur frequently in legacy applications) remain vulnerable.

The Double Submit Cookie pattern provides a stateless validation mechanism. When a user authenticates, the server generates a cryptographically secure, random value (the CSRF token) and writes it to a client cookie. Simultaneously, when the client makes a state-changing request, the application frontend reads the token value from the cookie and copies it into a custom HTTP request header (e.g., X-CSRF-Token).

Because the Same-Origin Policy (SOP) prevents cross-origin scripts from reading or writing cookies associated with the target domain, an attacker operating from a malicious origin cannot read the cookie to populate the custom header. When the request reaches the server, the middleware extracts the token from both the cookie and the header, verifying their mathematical equivalence. If they do not match, or if either is missing, the request is rejected immediately.

+-------------------------------------------------------------+
|                     Client Browser                          |
|                                                             |
|  1. Request Page  =======================================>  |
|  2. Receive CSRF Cookie & Store                             |
|                                                             |
|  3. Submit Form / API Request                               |
|     - Cookie: __Host-csrf-token=<token_val>                 |
|     - Header: X-CSRF-Token: <token_val>                     |
|                                                             |
+------------------------------++-----------------------------+
                               ||
                               || (HTTPS Request)
                               \/
+-------------------------------------------------------------+
|                      Server (Bun Runtime)                   |
|                                                             |
|  4. Extract Cookie Value & Header Value                     |
|  5. Cryptographic Timing-Safe Comparison                    |
|  6. Reject if Mismatched or Missing                         |
|                                                             |
+-------------------------------------------------------------+

Bun/TypeScript Production Implementation

Below is a complete, fully-typed TypeScript middleware implementation designed for the Bun runtime. This implementation does not rely on external web frameworks; it uses native Bun HTTP APIs and Node.js-compatible cryptographic modules to enforce timing-safe token verification and set secure cookie headers.

import { randomBytes, timingSafeEqual } from "node:crypto";

export interface CSRFOptions {
  cookieName?: string;
  headerName?: string;
  tokenLength?: number;
  cookieOptions?: {
    domain?: string;
    path?: string;
    secure?: boolean;
    sameSite?: "Lax" | "Strict" | "None";
    maxAge?: number;
  };
  ignoredMethods?: string[];
}

export class CSRFProtector {
  private cookieName: string;
  private headerName: string;
  private tokenLength: number;
  private ignoredMethods: Set<string>;
  private cookieOptions: Required<NonNullable<CSRFOptions["cookieOptions"]>>;

  constructor(options: CSRFOptions = {}) {
    this.cookieName = options.cookieName || "__Host-csrf-token";
    this.headerName = options.headerName || "x-csrf-token";
    this.tokenLength = options.tokenLength || 32;
    this.ignoredMethods = new Set(
      (options.ignoredMethods || ["GET", "HEAD", "OPTIONS", "TRACE"]).map((m) =>
        m.toUpperCase()
      )
    );

    this.cookieOptions = {
      domain: options.cookieOptions?.domain || "",
      path: options.cookieOptions?.path || "/",
      secure: options.cookieOptions?.secure !== false, // Default to true
      sameSite: options.cookieOptions?.sameSite || "Strict",
      maxAge: options.cookieOptions?.maxAge || 7200, // 2 hours
    };
  }

  /**
   * Generates a cryptographically secure random token.
   */
  public generateToken(): string {
    return randomBytes(this.tokenLength).toString("hex");
  }

  /**
   * Parses cookies manually from the raw Cookie header value.
   */
  private parseCookies(cookieHeader: string | null): Record<string, string> {
    const cookies: Record<string, string> = {};
    if (!cookieHeader) return cookies;

    const pairs = cookieHeader.split(";");
    for (const pair of pairs) {
      const splitPoint = pair.indexOf("=");
      if (splitPoint === -1) continue;
      const key = pair.substring(0, splitPoint).trim();
      const val = pair.substring(splitPoint + 1).trim();
      cookies[key] = decodeURIComponent(val);
    }
    return cookies;
  }

  /**
   * Serializes a cookie name and value into a Set-Cookie header string.
   */
  private serializeCookie(name: string, value: string): string {
    const parts = [`${name}=${encodeURIComponent(value)}`];
    
    if (this.cookieOptions.maxAge > 0) {
      parts.push(`Max-Age=${this.cookieOptions.maxAge}`);
    }
    if (this.cookieOptions.path) {
      parts.push(`Path=${this.cookieOptions.path}`);
    }
    if (this.cookieOptions.domain) {
      parts.push(`Domain=${this.cookieOptions.domain}`);
    }
    if (this.cookieOptions.secure) {
      parts.push("Secure");
    }
    if (this.cookieOptions.sameSite) {
      parts.push(`SameSite=${this.cookieOptions.sameSite}`);
    }
    parts.push("HttpOnly");

    return parts.join("; ");
  }

  /**
   * Executes timing-safe string comparison to mitigate side-channel timing attacks.
   */
  private compareTokens(tokenA: string, tokenB: string): boolean {
    const bufA = Buffer.from(tokenA, "utf-8");
    const bufB = Buffer.from(tokenB, "utf-8");

    if (bufA.length !== bufB.length) {
      return false;
    }
    return timingSafeEqual(bufA, bufB);
  }

  /**
   * Main middleware entrypoint to intercept and validate incoming Bun requests.
   */
  public async handle(
    request: Request,
    next: (req: Request) => Promise<Response>
  ): Promise<Response> {
    const method = request.method.toUpperCase();
    const cookies = this.parseCookies(request.headers.get("cookie"));
    
    // 1. Perform cookie injection for state-refreshing endpoints or initial visits
    let clientToken = cookies[this.cookieName];
    let tokenInjected = false;

    if (!clientToken) {
      clientToken = this.generateToken();
      tokenInjected = true;
    }

    // 2. Validate token for state-changing HTTP methods
    if (!this.ignoredMethods.has(method)) {
      const headerToken = request.headers.get(this.headerName);

      if (!clientToken || !headerToken) {
        return new Response(
          JSON.stringify({ error: "CSRF token validation failed: Missing token." }),
          { status: 403, headers: { "Content-Type": "application/json" } }
        );
      }

      if (!this.compareTokens(clientToken, headerToken)) {
        return new Response(
          JSON.stringify({ error: "CSRF token validation failed: Mismatched token." }),
          { status: 403, headers: { "Content-Type": "application/json" } }
        );
      }
    }

    // 3. Forward request execution to the next handler
    const response = await next(request);

    // 4. Inject the cookie into the response if it was missing or newly generated
    if (tokenInjected) {
      response.headers.append(
        "Set-Cookie",
        this.serializeCookie(this.cookieName, clientToken)
      );
    }

    return response;
  }
}

// Example usage within a native Bun.serve config
const csrfProtector = new CSRFProtector({
  cookieName: "__Host-csrf-token",
  headerName: "x-csrf-token",
  cookieOptions: {
    sameSite: "Strict",
    secure: true,
    path: "/",
  },
});

Bun.serve({
  port: 3000,
  async fetch(req) {
    return csrfProtector.handle(req, async (passedReq) => {
      const url = new URL(passedReq.url);
      if (url.pathname === "/api/transfer" && passedReq.method === "POST") {
        const body = await passedReq.json();
        return new Response(
          JSON.stringify({ status: "success", transferred: body.amount }),
          { headers: { "Content-Type": "application/json" } }
        );
      }
      return new Response("Application Root", { status: 200 });
    });
  },
});

Security and Performance Metrics

When selecting a mitigation structure, developers must evaluate the performance and coverage tradeoffs of different settings. The table below outlines defensive capabilities and latency metrics under cross-site scenarios.

Defense ConfigurationCross-Site GET CoverageCross-Site POST CoverageToken Generation LatencyHeader/Cookie OverheadBrowser CompatibilitySubdomain Isolation
SameSite=LaxNot Protected (Allowed)Protected (Blocked)0 ns (Static flag)~15 bytesHigh (Modern Only)Vulnerable
SameSite=StrictFully Protected (Blocked)Protected (Blocked)0 ns (Static flag)~18 bytesHigh (Modern Only)Vulnerable
Double Submit CookieFully Protected (Stateless)Fully Protected (Stateless)~850 ns (64-char hex)~120 bytesHigh (Legacy & Modern)Vulnerable (without prefix)
Hybrid (SameSite + Double Submit)Fully Protected (Blocked)Fully Protected (Blocked)~850 ns (64-char hex)~135 bytesHigh (All Environments)Fully Secure (using __Host-)

Note: All latency figures are derived from benchmarking Bun 1.1 running on an Intel Xeon CPU @ 3.40GHz. Token generation scales linearly with byte complexity.


What Breaks in Production

Security configurations that function correctly during local development often break in production when exposed to multi-domain deployment topology, legacy web clients, and transport layer modifications.

The Same-Origin Policy (SOP) restricts scripts from accessing data on other origins, but cookie visibility rules operate under a different schema. By default, any subdomain within a domain hierarchy (e.g., staging.app.example.com) can write cookies for the parent domain (example.com).

If an attacker compromises a minor subdomain, they can write a dummy cookie with a known CSRF token to example.com. When the victim visits the primary application (app.example.com), the browser sends both the legitimate cookie and the attacker’s injected cookie. Because browsers do not dictate which cookie takes precedence when duplicates exist, the application might validate the incoming request against the attacker’s injected cookie. The attacker can then issue a cross-site request with their known CSRF token in the HTTP headers, bypassing the defense.

Remediation:

  • Prepend the __Host- prefix to the CSRF cookie name (e.g., __Host-csrf-token). Cookies using the __Host- prefix are restricted to the origin that set them, cannot define a Domain attribute (meaning they cannot be shared across subdomains), and must have the Secure flag.
  • Enforce the Path=/ configuration to restrict path scope.

2. Protocol Downgrade Attacks (HTTP to HTTPS)

If an application allows HTTP connections or fails to enforce Strict Transport Security (HSTS), a network attacker can execute a protocol downgrade attack. When the client makes an unencrypted HTTP request, the attacker can intercept the request and inject a spoofed HTTP cookie with a known CSRF token. When the client subsequent navigates back to the secure HTTPS site, the browser transmits the attacker’s injected cookie alongside the legitimate one.

Remediation:

  • Set the Secure flag on all CSRF cookies. The browser will block the transmission of these cookies over unencrypted connections.
  • Deploy HSTS headers with the includeSubDomains directive:
    Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
    
  • Reject any state-changing actions arriving from non-TLS connection pathways inside the middleware boundary.

Older web browsers do not recognize the SameSite attribute. When a browser encounters an attribute it cannot parse, it ignores it. In this scenario, the cookie behaves as if SameSite were not set, reverting to the default browser behavior (which historically meant sending cookies on all cross-site requests). In addition, some older versions of Safari (specifically on iOS 12 and macOS 10.14) contain a bug where they treat SameSite=None as if it were SameSite=Strict, locking users out of legitimate cross-site workflows.

Remediation:

  • Implement the Double Submit Cookie pattern as a fallback verification layer. Do not rely exclusively on the SameSite attribute.
  • Use user-agent sniffing if cross-site tracking capabilities are required, and deploy a dual-cookie strategy where one cookie has the SameSite attribute and the other does not, allowing the server to dynamically select the appropriate credential based on user agent indicators.

FAQs

What is the primary benefit of this design pattern?

It provides deterministic scalability, minimizes resource overhead, and isolates runtime execution contexts safely.

How do we verify the performance improvements?

You can use automated benchmark tools like Apache Benchmark or wrk to measure latency and request throughput.

Frequently Asked Questions

What is the primary benefit of this design pattern?

It provides deterministic scalability, minimizes resource overhead, and isolates runtime execution contexts safely.

How do we verify the performance improvements?

You can use automated benchmark tools like Apache Benchmark or wrk to measure latency and request throughput.