Skip to content

sec(auth): CORS wildcard origin + credentials=true on staging Lambda URL #390

Description

@cristim

Summary

The staging Lambda Function URL is configured with allowed_origins = ["*"] (wildcard) in terraform/environments/aws/github-staging.tfvars while simultaneously setting Access-Control-Allow-Credentials: true via the cors { allow_credentials = true } block in the Lambda Terraform module (terraform/modules/compute/aws/lambda/main.tf). AWS Lambda Function URLs enforce CORS at the infrastructure layer before the Go application code runs: an Origin request header causes AWS to emit Access-Control-Allow-Origin: <reflected-origin> + Access-Control-Allow-Credentials: true in every response, regardless of what the Go handler sets. Dynamic probing confirms this: curl -H "Origin: https://attacker.io" https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws/api/health returns both the configured origin AND the attacker's origin, each with Access-Control-Allow-Credentials: true.

Per the CORS specification, a browser will forward cookies/credentials to a cross-origin response only when Access-Control-Allow-Origin exactly matches the request's Origin AND Allow-Credentials is true. With a wildcard origin the spec says browsers must not expose credentials — but this deployment is not using the literal * response value; AWS is reflecting the inbound Origin verbatim, which turns it into a fully-trusted origin. Any website on the internet can therefore issue fetch("https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws/api/users", {credentials:"include"}) and the browser will attach the victim's session token, bypassing the CSRF defence.

Risk: full account takeover if a CUDly admin can be tricked into visiting an attacker-controlled page while logged in. Staging is used by the CI pipeline and by engineers validating changes before production — the attack surface is real.

Location

  • terraform/environments/aws/github-staging.tfvars line 28: lambda_allowed_origins = ["*"]
  • terraform/modules/compute/aws/lambda/main.tf: cors { allow_credentials = true; allow_origins = var.allowed_origins }

Reproduction / PoC

# Confirm reflected Origin + Allow-Credentials: true
curl -s -i -X OPTIONS \
  -H "Origin: https://attacker.io" \
  -H "Access-Control-Request-Method: POST" \
  "https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws/api/auth/login" \
  | grep -i "access-control"
# Returns:
# Access-Control-Allow-Origin: https://attacker.io  ← reflected
# Access-Control-Allow-Credentials: true

# Cross-origin credential read PoC (browser):
# fetch("https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws/api/auth/me",
#   { credentials: "include" })
#   .then(r => r.json())
#   .then(console.log)  // leaks full user profile

Suggested fix

  1. Replace lambda_allowed_origins = ["*"] with the actual CloudFront/dashboard origin in every environment's tfvars. For staging this should be the staging CloudFront domain or the Lambda URL itself:
    lambda_allowed_origins = ["https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws"]
  2. Add a Terraform validation rule to the allowed_origins variable that rejects ["*"] when allow_credentials = true:
    validation {
      condition = !(contains(var.allowed_origins, "*") && var.function_url_allow_credentials)
      error_message = "Wildcard origin is incompatible with allow_credentials = true."
    }
  3. Consider adding an integration test that hits the Lambda preflight with a random origin and asserts the response does NOT reflect it.

Severity rationale

CORS wildcard + credentials = any-origin cross-site request forgery without a CSRF token. While the Go application also enforces per-session CSRF tokens for state-changing requests, the reflected-origin response means a cross-origin browser fetch with credentials: "include" reads the session token from a prior JSON response (e.g., /api/auth/me) and can then supply its own CSRF header — full bypass. Priority P1 because staging is actively used by the CI pipeline and by engineers.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions