How to Configure IIS CORS Headers on Windows Server 2025

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls how web pages hosted on one origin (scheme + hostname + port) can request resources from a different origin. When a JavaScript application served from https://app.example.com tries to call an API at https://api.example.com, the browser first checks whether the server permits the cross-origin request by inspecting HTTP response headers. Without the correct CORS headers, the browser blocks the response even if the server returned HTTP 200. On Windows Server 2025 with IIS, CORS headers can be configured through web.config, the IIS CORS module, or custom HTTP modules. This tutorial covers all three approaches, along with testing techniques and guidance for handling the OPTIONS preflight request.

Prerequisites

  • Windows Server 2025 with IIS 10 installed
  • Administrative access to IIS Manager and the site’s physical path
  • PowerShell 5.1 or later (for command-line configuration)
  • The IIS CORS module if you want the dynamic-origin approach (downloaded separately)
  • A browser or curl for testing CORS headers

Step 1: Understanding the CORS Flow

Before configuring anything, it helps to understand what IIS must return for CORS to work correctly.

For simple requests (GET, POST with certain content types), the browser sends the request with an Origin header and expects the response to include Access-Control-Allow-Origin matching that origin (or *).

For preflighted requests (DELETE, PUT, PATCH, or requests with custom headers like Authorization or Content-Type: application/json), the browser first sends an HTTP OPTIONS request to the same URL. The server must respond with the permitted methods, headers, and optionally a max-age cache duration — all before the actual request is sent. If IIS returns a 404 or 405 for OPTIONS, CORS fails even if the GET/POST would succeed.

Step 2: Configuring CORS Headers via web.config

The simplest approach for a single known origin is to add custom HTTP headers directly in web.config. Open or create the file in your IIS site’s root directory:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>

    <!-- Add CORS response headers -->
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin"   value="https://app.example.com" />
        <add name="Access-Control-Allow-Methods"  value="GET, POST, PUT, DELETE, OPTIONS" />
        <add name="Access-Control-Allow-Headers"  value="Content-Type, Authorization, X-Requested-With" />
        <add name="Access-Control-Allow-Credentials" value="true" />
        <add name="Access-Control-Max-Age"        value="86400" />
      </customHeaders>
    </httpProtocol>

    <!-- Return 200 OK for OPTIONS preflight without hitting application code -->
    <handlers>
      <add name="OPTIONSVerbHandler"
           path="*"
           verb="OPTIONS"
           modules="ProtocolSupportModule"
           requireAccess="None" />
    </handlers>

  </system.webServer>
</configuration>

Key header explanations:

  • Access-Control-Allow-Origin — the exact origin permitted (or * for public APIs that don’t use credentials)
  • Access-Control-Allow-Methods — HTTP verbs the client may use
  • Access-Control-Allow-Headers — request headers the client may include
  • Access-Control-Allow-Credentials — must be true if the client sends cookies or Authorization headers; cannot be combined with Access-Control-Allow-Origin: *
  • Access-Control-Max-Age — seconds the browser may cache the preflight result (86400 = 24 hours)

Step 3: Installing the IIS CORS Module for Dynamic Origins

The static web.config approach only works for a single fixed origin. For APIs that must allow multiple origins (e.g., a staging domain and a production domain), install Microsoft’s official IIS CORS module.

# Download the IIS CORS module installer
Invoke-WebRequest `
    -Uri "https://download.microsoft.com/download/iis-cors-module/IISCORS_amd64.msi" `
    -OutFile "$env:TEMPIISCORS_amd64.msi"

# Install silently
Start-Process msiexec.exe `
    -ArgumentList "/i `"$env:TEMPIISCORS_amd64.msi`" /quiet /norestart" `
    -Wait

# Restart IIS to load the new module
iisreset /restart

After installation, the module appears under IIS Manager > Modules as IISCorsModule. Configure it in web.config:

<system.webServer>
  <cors enabled="true" failUnlistedOrigins="true">
    <add origin="https://app.example.com"
         allowCredentials="true"
         maxAge="7200">
      <allowHeaders allowAllRequestedHeaders="true" />
      <allowMethods>
        <add method="GET" />
        <add method="POST" />
        <add method="PUT" />
        <add method="DELETE" />
        <add method="OPTIONS" />
      </allowMethods>
    </add>
    <add origin="https://staging.example.com"
         allowCredentials="true"
         maxAge="3600">
      <allowHeaders allowAllRequestedHeaders="true" />
      <allowMethods>
        <add method="GET" />
        <add method="POST" />
      </allowMethods>
    </add>
  </cors>
</system.webServer>

With failUnlistedOrigins="true", any request from an origin not in the list receives a response without CORS headers, causing the browser to block it. Setting it to false silently permits unlisted origins — not recommended for production.

Step 4: Handling OPTIONS Preflight Requests

A common failure mode is IIS returning 404 or 405 for the OPTIONS verb because ASP.NET or a downstream handler intercepts it before CORS headers can be added. Verify OPTIONS handling with PowerShell:

Invoke-WebRequest `
    -Method OPTIONS `
    -Uri "https://api.example.com/data" `
    -Headers @{ "Origin" = "https://app.example.com";
                "Access-Control-Request-Method"  = "POST";
                "Access-Control-Request-Headers" = "Content-Type,Authorization" } `
    -UseBasicParsing | Select-Object StatusCode, Headers

If OPTIONS returns anything other than 200 or 204, add a dedicated handler in web.config that short-circuits before ASP.NET routing:

<handlers>
  <!-- Must be first in the list -->
  <add name="OPTIONSHandler"
       path="*"
       verb="OPTIONS"
       type="System.Web.Handlers.TransferRequestHandler"
       preCondition="integratedMode,runtimeVersionv4.0"
       modules="ProtocolSupportModule"
       requireAccess="None" />
</handlers>

For Web API or ASP.NET Core applications, also ensure the CORS middleware is applied before routing middleware in the application pipeline so the framework itself does not reject the preflight.

Step 5: Per-Application vs Global web.config

CORS headers set in the site root web.config apply only to that site. To apply CORS headers globally across all IIS sites on the server, configure them in %SystemRoot%System32inetsrvconfigapplicationHost.config:

Import-Module WebAdministration

# Add a global custom header at the server level
Add-WebConfigurationProperty `
    -PSPath "IIS:" `
    -Filter "system.webServer/httpProtocol/customHeaders" `
    -Name "." `
    -Value @{
        name  = "Access-Control-Allow-Origin"
        value = "https://app.example.com"
    }

Be cautious with global CORS headers: they will be sent on every response from every site, including admin interfaces where you may not want cross-origin access. Per-site web.config configuration is generally safer.

Step 6: Testing CORS Configuration

Test from the command line using curl to simulate a browser preflight:

# Simulate preflight OPTIONS request
curl -v -X OPTIONS "https://api.example.com/v1/users" `
     -H "Origin: https://app.example.com" `
     -H "Access-Control-Request-Method: POST" `
     -H "Access-Control-Request-Headers: Content-Type,Authorization"

# Expected response headers:
# Access-Control-Allow-Origin:   https://app.example.com
# Access-Control-Allow-Methods:  GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers:  Content-Type, Authorization
# Access-Control-Max-Age:        86400

In the browser, open Developer Tools (F12) > Network tab > filter by your API hostname. Look for the OPTIONS preflight request and confirm all required Access-Control-* headers are present in the response. If headers are missing or the origin does not match exactly (including scheme and port), the browser will display a CORS error in the Console tab.

Conclusion

Correctly configuring CORS on IIS requires understanding both the browser’s preflight mechanism and IIS’s handler pipeline order. For single-origin scenarios, static web.config custom headers combined with a short-circuit OPTIONS handler are sufficient and require no additional software. For multi-origin or dynamic-origin requirements, the Microsoft IIS CORS module provides a clean declarative configuration with per-origin granularity. Always test both simple and preflighted requests with curl or browser DevTools after any CORS configuration change, and avoid using the wildcard Access-Control-Allow-Origin: * on APIs that handle authenticated sessions or sensitive data.