CrackedRuby CrackedRuby

Overview

Cross-Origin Resource Sharing (CORS) defines a protocol for browsers to determine whether cross-origin requests from web applications should be permitted. By default, browsers enforce the same-origin policy, which prevents JavaScript code from one origin from accessing resources at a different origin. CORS provides a controlled way to relax this restriction through a series of HTTP headers exchanged between the browser and server.

An origin consists of three components: protocol (scheme), domain (host), and port. Two URLs have the same origin only when all three components match exactly. For example, https://api.example.com:443/data and https://example.com:443/data have different origins due to different subdomains, even though they share the same protocol and port.

CORS applies only to requests initiated by browser-based code such as XMLHttpRequest or Fetch API calls. It does not affect server-to-server communication or requests from non-browser clients. The mechanism protects users by preventing malicious websites from making unauthorized requests on their behalf to other sites where they might be authenticated.

The CORS protocol distinguishes between simple requests and preflighted requests. Simple requests meet specific criteria regarding HTTP methods, headers, and content types. Browsers send these directly with an Origin header. Preflighted requests involve an additional OPTIONS request that the browser sends before the actual request to verify permissions. The server responds to this preflight with headers indicating which origins, methods, and headers are allowed.

# Example origins and their relationships
origin_a = "https://app.example.com"
origin_b = "https://app.example.com:443"  # Same as origin_a (default HTTPS port)
origin_c = "http://app.example.com"       # Different (protocol mismatch)
origin_d = "https://api.example.com"      # Different (subdomain mismatch)
origin_e = "https://app.example.com:8443" # Different (port mismatch)

Key Principles

CORS operates through a request-response header exchange between the browser and server. When a browser makes a cross-origin request, it includes an Origin header identifying the requesting page's origin. The server examines this header and decides whether to permit the request by including appropriate CORS headers in the response.

The fundamental CORS header is Access-Control-Allow-Origin, which specifies which origins can access the resource. The server can return a specific origin, a wildcard * to allow any origin, or omit the header entirely to deny access. When credentials are involved (cookies, HTTP authentication), the wildcard is not permitted, and the server must explicitly name the allowed origin.

Simple requests bypass the preflight mechanism when they meet these conditions: the HTTP method is GET, HEAD, or POST; only simple headers are used (Accept, Accept-Language, Content-Language, Content-Type); and if Content-Type is present, its value is application/x-www-form-urlencoded, multipart/form-data, or text/plain. Any deviation from these criteria triggers a preflight.

The preflight request uses the OPTIONS method and includes Access-Control-Request-Method and optionally Access-Control-Request-Headers to inform the server which method and headers the actual request will use. The server responds with headers indicating permissions: Access-Control-Allow-Methods lists permitted methods, Access-Control-Allow-Headers lists permitted headers, and Access-Control-Max-Age specifies how long the preflight response can be cached.

Credentials present a special case in CORS. By default, cross-origin requests do not include credentials. The client must explicitly set the credentials flag, and the server must respond with Access-Control-Allow-Credentials: true. When credentials are involved, Access-Control-Allow-Origin cannot be *, and Access-Control-Allow-Headers and Access-Control-Allow-Methods cannot use wildcards either.

# Simple request - no preflight needed
# GET request with standard headers
response = HTTP.get("https://api.other-domain.com/data")

# Preflighted request - OPTIONS sent first
# PUT request with custom header triggers preflight
response = HTTP.headers("X-Custom-Header" => "value")
               .put("https://api.other-domain.com/data", json: {data: "value"})

CORS provides additional headers for fine-grained control. Access-Control-Expose-Headers lists response headers that the browser should make available to the requesting code, as browsers only expose simple response headers by default. Access-Control-Allow-Private-Network addresses requests to private network resources from public websites, adding another security boundary.

The browser enforces CORS restrictions, not the server. The server receives and processes requests regardless of CORS headers. CORS prevents the browser JavaScript from reading the response if the headers are incorrect. This means CORS does not prevent the request from executing; it only prevents the client code from accessing the response data.

Ruby Implementation

Ruby web applications typically implement CORS through Rack middleware, which intercepts HTTP requests and adds appropriate headers before passing responses back to the client. The most common approach uses the rack-cors gem, which provides a domain-specific language for configuring CORS policies.

# Gemfile
gem 'rack-cors'

# config.ru or Rails config/application.rb
require 'rack/cors'

use Rack::Cors do
  allow do
    origins 'https://frontend.example.com'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true
  end
end

Rails applications can configure CORS in the initializers directory, creating a dedicated configuration file that gets loaded during application startup. The middleware integrates seamlessly with the Rails middleware stack.

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3000', '127.0.0.1:3000', 'https://app.example.com'
    
    resource '/api/*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true,
      max_age: 86400
  end
  
  allow do
    origins '*'
    
    resource '/public/*',
      headers: :any,
      methods: [:get, :head],
      credentials: false
  end
end

The origins method accepts multiple forms: a single string, an array of strings, a regular expression, or a block that receives the origin and returns a boolean. Regular expressions provide flexible matching for subdomain patterns or multiple environments.

# Regular expression for subdomain matching
allow do
  origins /\Ahttps:\/\/.*\.example\.com\z/
  resource '*', headers: :any, methods: [:get, :post]
end

# Block-based dynamic origin validation
allow do
  origins do |source, env|
    # Check against database of registered client origins
    AllowedOrigin.exists?(url: source)
  end
  resource '*', headers: :any, methods: [:get, :post]
end

Resource-level configuration controls which paths and methods are exposed. The resource method accepts a path pattern and options hash. Multiple resource blocks enable different policies for different API endpoints.

allow do
  origins 'https://trusted.example.com'
  
  # Public read-only endpoints
  resource '/api/v1/public/*',
    headers: :any,
    methods: [:get, :head],
    credentials: false
  
  # Authenticated endpoints
  resource '/api/v1/users/*',
    headers: :any,
    methods: [:get, :post, :put, :delete, :options],
    credentials: true,
    expose: ['X-Total-Count', 'X-Page']
  
  # Admin endpoints with additional restrictions
  resource '/api/v1/admin/*',
    headers: ['Authorization', 'Content-Type'],
    methods: [:get, :post, :put, :delete],
    credentials: true
end

Manual header implementation offers more control for applications not using rack-cors. This approach sets headers directly in controller actions or middleware.

# Manual CORS headers in Rails controller
class ApiController < ApplicationController
  before_action :set_cors_headers
  
  def set_cors_headers
    allowed_origins = [
      'https://app.example.com',
      'https://staging.example.com'
    ]
    
    origin = request.headers['Origin']
    
    if allowed_origins.include?(origin)
      response.headers['Access-Control-Allow-Origin'] = origin
      response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
      response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization'
      response.headers['Access-Control-Allow-Credentials'] = 'true'
      response.headers['Access-Control-Max-Age'] = '86400'
    end
  end
  
  # Handle preflight requests
  def preflight
    head :ok
  end
end

# routes.rb
match '*path', to: 'api#preflight', via: :options

Sinatra applications configure CORS similarly, either through rack-cors in the config.ru file or manually in before filters.

# Sinatra with rack-cors
require 'sinatra'
require 'rack/cors'

use Rack::Cors do
  allow do
    origins 'https://frontend.example.com'
    resource '*', headers: :any, methods: [:get, :post, :delete]
  end
end

# Manual Sinatra CORS
before do
  origin = request.env['HTTP_ORIGIN']
  allowed = ['https://frontend.example.com', 'https://staging.frontend.com']
  
  if allowed.include?(origin)
    headers 'Access-Control-Allow-Origin' => origin,
            'Access-Control-Allow-Methods' => 'GET, POST, DELETE, OPTIONS',
            'Access-Control-Allow-Headers' => 'Content-Type, Accept',
            'Access-Control-Allow-Credentials' => 'true'
  end
end

options '*' do
  200
end

Security Implications

CORS misconfigurations create significant security vulnerabilities. The most critical mistake involves using Access-Control-Allow-Origin: * with credentials enabled, which browsers reject. However, dynamically reflecting the Origin header without validation effectively creates the same vulnerability while bypassing the browser's protection.

# DANGEROUS: Reflects any origin
before_action :set_cors_headers

def set_cors_headers
  # Accepts any origin - major security flaw
  response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
  response.headers['Access-Control-Allow-Credentials'] = 'true'
end

This pattern allows any website to make authenticated requests to the API, potentially exposing user data or enabling unauthorized actions. An attacker hosts a malicious site that makes requests to the API, and because the server reflects the attacker's origin, the browser permits the request and includes the user's cookies.

Proper origin validation requires explicit allowlisting. Applications should maintain a list of permitted origins and verify the request origin against this list before setting the CORS headers.

# Secure origin validation
ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://app.example.co.uk',
  'https://mobile.example.com'
].freeze

before_action :set_cors_headers

def set_cors_headers
  origin = request.headers['Origin']
  
  if ALLOWED_ORIGINS.include?(origin)
    response.headers['Access-Control-Allow-Origin'] = origin
    response.headers['Access-Control-Allow-Credentials'] = 'true'
  end
end

Regular expression validation requires careful construction to prevent bypasses. Poorly written patterns can match unintended origins.

# DANGEROUS: Weak regex allows subdomain.evil.com
origins /example\.com/

# SAFE: Anchored regex with proper escaping
origins /\Ahttps:\/\/([a-z0-9-]+\.)?example\.com\z/

The preflight cache presents a security consideration. Long cache times improve performance but prevent rapid policy changes. If an origin becomes compromised and needs immediate removal from the allowlist, cached preflights continue allowing requests until the cache expires. Balance performance against the ability to revoke access quickly.

CORS does not prevent requests from being sent and processed. A common misconception holds that CORS prevents unauthorized access to APIs. CORS only controls whether browser JavaScript can read the response. State-changing operations execute regardless of CORS headers. Applications must implement authentication and authorization separately from CORS.

# CORS does not replace authentication
class UsersController < ApplicationController
  before_action :set_cors_headers
  before_action :authenticate_user!  # Still required
  before_action :authorize_user!     # Still required
  
  def update
    # Request processes even with CORS failure
    # Must validate user permissions
    if current_user.can_update?(@user)
      @user.update(user_params)
    end
  end
end

Exposing sensitive information through headers requires careful consideration. The Access-Control-Expose-Headers configuration determines which response headers JavaScript can read. Accidentally exposing headers containing sensitive data or internal system information creates information disclosure vulnerabilities.

# Review exposed headers carefully
resource '/api/*',
  expose: ['X-Total-Count', 'X-Request-ID'],  # Safe
  # Avoid: 'X-Internal-Server', 'X-Database-Query-Time'
  credentials: true

Subdomain attacks exploit overly permissive origin matching. If an attacker controls any subdomain and the CORS policy allows all subdomains, they can make requests from the compromised subdomain. Wildcard subdomain matching requires confidence in subdomain security.

Implementation Approaches

CORS implementation strategies vary based on application architecture, deployment environment, and security requirements. The middleware approach centralizes CORS handling, making policies consistent across all endpoints and separating concerns between business logic and cross-origin handling.

Middleware-based CORS works well for APIs serving multiple client applications. Configuration lives in a single location, and changes propagate automatically to all endpoints. This approach handles preflight requests transparently without requiring explicit OPTIONS route definitions.

# Middleware approach - centralized configuration
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  # Development environment - permissive
  if Rails.env.development?
    allow do
      origins 'localhost:3000', '127.0.0.1:3000'
      resource '*', headers: :any, methods: :any
    end
  end
  
  # Production environment - restrictive
  if Rails.env.production?
    allow do
      origins ENV.fetch('ALLOWED_ORIGINS', '').split(',')
      resource '/api/v1/*',
        headers: :any,
        methods: [:get, :post, :put, :delete, :options],
        credentials: true,
        max_age: 86400
    end
  end
end

Controller-level implementation provides granular control when different endpoints require different CORS policies. This approach fits applications where certain controllers serve public data while others handle authenticated operations.

# Controller-level approach
class PublicApiController < ApplicationController
  before_action :set_public_cors
  
  def set_public_cors
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET, HEAD'
  end
end

class AuthenticatedApiController < ApplicationController
  before_action :set_authenticated_cors
  
  TRUSTED_ORIGINS = ENV['TRUSTED_ORIGINS'].split(',').freeze
  
  def set_authenticated_cors
    origin = request.headers['Origin']
    if TRUSTED_ORIGINS.include?(origin)
      response.headers['Access-Control-Allow-Origin'] = origin
      response.headers['Access-Control-Allow-Credentials'] = 'true'
    end
  end
end

Proxy-based CORS handling moves policy enforcement to a reverse proxy or API gateway. This approach removes CORS handling from application code entirely, making it the infrastructure's responsibility. Nginx, Apache, or cloud API gateways enforce CORS policies before requests reach the application.

# Nginx CORS configuration
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
    
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
    add_header 'Access-Control-Allow-Credentials' 'true';
    
    proxy_pass http://application_server;
}

Dynamic origin validation against a database supports applications where clients register dynamically. This pattern requires a query per request but enables flexible client management without redeployment.

# Database-backed origin validation
class AllowedOrigin < ApplicationRecord
  validates :origin, presence: true, uniqueness: true
  validates :origin, format: { with: URI::DEFAULT_PARSER.make_regexp(['http', 'https']) }
end

use Rack::Cors do
  allow do
    origins do |source, env|
      AllowedOrigin.exists?(origin: source, active: true)
    end
    resource '*', headers: :any, methods: :any, credentials: true
  end
end

Environment-based configuration separates CORS policies across deployment environments. Development environments use permissive policies for convenience, while production environments enforce strict controls.

# Environment-specific configuration
class CorsConfiguration
  def self.allowed_origins
    case Rails.env
    when 'development'
      ['http://localhost:3000', 'http://localhost:8080']
    when 'staging'
      ['https://staging.example.com']
    when 'production'
      ['https://app.example.com', 'https://www.example.com']
    end
  end
end

use Rack::Cors do
  allow do
    origins CorsConfiguration.allowed_origins
    resource '*', headers: :any, methods: :any, credentials: true
  end
end

Practical Examples

A typical single-page application scenario involves a React or Vue.js frontend hosted on one domain making API requests to a Rails backend on a different domain. The frontend runs on https://app.example.com while the API serves from https://api.example.com.

# API server configuration
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'https://app.example.com'
    
    resource '/api/v1/*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      credentials: true,
      expose: ['X-Total-Count', 'X-Per-Page'],
      max_age: 600
  end
end

# Frontend JavaScript making authenticated request
fetch('https://api.example.com/api/v1/users', {
  method: 'GET',
  credentials: 'include',  // Sends cookies
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + authToken
  }
})

Multiple frontend applications accessing the same API require careful origin management. An organization might have production, staging, and development environments plus mobile web views, each needing access.

# Multiple environment support
class CorsOrigins
  PRODUCTION = [
    'https://app.example.com',
    'https://www.example.com',
    'https://mobile.example.com'
  ].freeze
  
  STAGING = [
    'https://staging-app.example.com',
    'https://staging-mobile.example.com'
  ].freeze
  
  DEVELOPMENT = [
    'http://localhost:3000',
    'http://localhost:8080',
    'http://192.168.1.100:3000'  # Local network testing
  ].freeze
  
  def self.all
    case Rails.env
    when 'production'
      PRODUCTION
    when 'staging'
      STAGING + PRODUCTION  # Staging can access both
    when 'development', 'test'
      DEVELOPMENT + STAGING + PRODUCTION  # Dev has access to all
    end
  end
end

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins CorsOrigins.all
    resource '/api/*', headers: :any, methods: :any, credentials: true
  end
end

File upload scenarios present specific challenges because they often use custom content types and may exceed simple request criteria. An application allowing users to upload images requires proper CORS configuration for preflight requests.

# File upload endpoint CORS
allow do
  origins 'https://app.example.com'
  
  resource '/api/v1/uploads',
    headers: ['Content-Type', 'Content-Disposition', 'Accept'],
    methods: [:post, :options],
    credentials: true,
    max_age: 1800
end

# Controller handling upload
class UploadsController < ApplicationController
  def create
    uploaded_file = params[:file]
    
    if uploaded_file.content_type.start_with?('image/')
      # Process upload
      render json: { url: uploaded_file.url }, status: :created
    else
      render json: { error: 'Invalid file type' }, status: :unprocessable_entity
    end
  end
end

Third-party widget integration demonstrates CORS configuration for embedded components. A widget hosted on widget.example.com needs to communicate with the main API while being embedded in customer sites.

# Widget API configuration
allow do
  # Widget origin
  origins 'https://widget.example.com'
  
  resource '/api/v1/widget/*',
    headers: :any,
    methods: [:get, :post],
    credentials: false,  # Public widget, no credentials
    max_age: 86400
end

# Widget initialization endpoint
class WidgetController < ApplicationController
  def initialize_widget
    render json: {
      widget_id: SecureRandom.uuid,
      config: WidgetConfiguration.for_domain(params[:domain])
    }
  end
  
  def track_event
    WidgetEvent.create!(
      widget_id: params[:widget_id],
      event_type: params[:event_type],
      data: params[:data]
    )
    head :ok
  end
end

Common Pitfalls

Wildcard origin with credentials represents the most common CORS mistake. Browsers explicitly forbid this combination, yet developers frequently attempt it hoping for maximum flexibility.

# FAILS: Browser rejects this combination
resource '*',
  headers: :any,
  methods: :any,
  credentials: true  # Cannot use wildcard with credentials: true

# Alternative that works but is insecure
allow do
  origins do |source, env|
    true  # Accepts any origin - security vulnerability
  end
  resource '*', credentials: true
end

Missing OPTIONS route handling causes preflight requests to fail with 404 responses. Applications must handle OPTIONS requests for any endpoint that might receive preflighted requests.

# Missing OPTIONS handling causes failures
resources :users do
  member do
    put :update  # Has route for PUT
    # Missing: match :update, via: :options
  end
end

# Correct implementation
resources :users do
  member do
    put :update
    match :update, via: :options  # Explicit OPTIONS route
  end
end

# Or use rack-cors which handles OPTIONS automatically

Incorrect header names in Access-Control-Allow-Headers prevent legitimate requests. The header names must exactly match what the client sends in Access-Control-Request-Headers during preflight.

# Client sends these headers
fetch(url, {
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': apiKey,
    'X-Request-ID': requestId
  }
})

# Server must allow exact names (case-insensitive)
resource '*',
  headers: ['Content-Type', 'X-API-Key', 'X-Request-ID']

# headers: :any works but is less secure

Caching preflight responses for too long prevents quick policy updates. When an origin becomes compromised, cached preflight responses continue allowing requests until expiry.

# Very long cache - inflexible
resource '*', max_age: 86400  # 24 hours

# Balanced approach
resource '/api/v1/public/*', max_age: 3600      # 1 hour for public
resource '/api/v1/users/*', max_age: 600        # 10 minutes for authenticated
resource '/api/v1/admin/*', max_age: 0          # No cache for admin

Forgetting to expose custom response headers prevents client code from accessing them. Browsers only expose simple response headers automatically.

# Server sends custom headers
def index
  response.headers['X-Total-Count'] = User.count.to_s
  response.headers['X-Page-Size'] = '20'
  render json: users
end

# But doesn't expose them - client cannot read
resource '*',
  methods: [:get],
  credentials: true
  # Missing: expose: ['X-Total-Count', 'X-Page-Size']

# Correct configuration
resource '*',
  methods: [:get],
  credentials: true,
  expose: ['X-Total-Count', 'X-Page-Size']

Development versus production configuration mismatches cause unexpected failures when deploying. Permissive development settings work locally but fail in production with stricter policies.

# Development - permissive, works everywhere
if Rails.env.development?
  allow do
    origins '*'
    resource '*', headers: :any, methods: :any
  end
end

# Production - but frontend URL was missed
if Rails.env.production?
  allow do
    origins 'https://api.example.com'  # Wrong - this is the API domain
    # Should be: 'https://app.example.com'  # The frontend domain
    resource '*', headers: :any, methods: :any
  end
end

Port mismatches cause origin comparison failures. Default ports (80 for HTTP, 443 for HTTPS) are equivalent to explicit port specifications, but non-standard ports must match exactly.

# These are equivalent - same origin
'https://example.com'
'https://example.com:443'

# These are different origins
'https://example.com'
'https://example.com:8443'

# Configuration must match exactly
allow do
  origins 'https://app.example.com:8080'  # Must include port if non-standard
  resource '*', methods: :any
end

Reference

CORS Headers

Header Direction Purpose
Origin Request Identifies the requesting origin
Access-Control-Allow-Origin Response Specifies allowed origin or wildcard
Access-Control-Allow-Methods Response Lists permitted HTTP methods
Access-Control-Allow-Headers Response Lists permitted request headers
Access-Control-Allow-Credentials Response Indicates if credentials are allowed
Access-Control-Expose-Headers Response Lists headers accessible to client code
Access-Control-Max-Age Response Specifies preflight cache duration in seconds
Access-Control-Request-Method Preflight Request Indicates intended method for actual request
Access-Control-Request-Headers Preflight Request Lists headers intended for actual request

Simple Request Criteria

Aspect Requirement
Method GET, HEAD, or POST only
Headers Accept, Accept-Language, Content-Language, Content-Type, Range
Content-Type Values application/x-www-form-urlencoded, multipart/form-data, text/plain
Event Listeners No ReadableStream used in request body
XMLHttpRequest Upload No event listeners registered

Rack::Cors Configuration Options

Option Type Description
origins String, Array, Regex, Proc Specifies allowed origins
resource String Path pattern for matching resources
methods Array, Symbol HTTP methods to allow
headers Array, Symbol Request headers to allow
expose Array Response headers to expose
credentials Boolean Whether to allow credentials
max_age Integer Preflight cache duration in seconds
if Proc Conditional logic for applying rules

Common Origin Patterns

# Single origin
origins 'https://app.example.com'

# Multiple origins
origins 'https://app.example.com', 'https://mobile.example.com'

# Array
origins ['https://app.example.com', 'https://www.example.com']

# All subdomains
origins /\Ahttps:\/\/([a-z0-9-]+\.)?example\.com\z/

# Dynamic validation
origins do |source, env|
  AllowedOrigin.exists?(origin: source)
end

# Environment-based
origins(Rails.env.production? ? 'https://app.example.com' : '*')

Security Checklist

Check Requirement
Origin Validation Explicit allowlist, not reflection
Wildcard Usage Never with credentials
Regex Patterns Properly anchored and escaped
Sensitive Headers Not included in expose list
State Changes Protected by authentication/authorization
Cache Duration Appropriate for security requirements
HTTPS Required for production credentials
Error Responses Do not reveal internal details

Troubleshooting Decision Tree

Symptom Likely Cause Solution
404 on preflight Missing OPTIONS route Add OPTIONS handler or use middleware
Origin null in logs File protocol or privacy mode Use local server, not file://
Request succeeds but response inaccessible Missing CORS headers Add Access-Control-Allow-Origin
Preflight succeeds but main request fails Method not allowed Add method to Access-Control-Allow-Methods
Cannot read custom headers Headers not exposed Add to Access-Control-Expose-Headers
Works in Postman but fails in browser CORS not configured Configure CORS headers
Wildcard rejected with credentials Invalid configuration Use specific origin or remove credentials
Works locally but fails in production Environment mismatch Verify production origin configuration