# CORS Auditor (`phoenix2810/cors-auditor`) Actor

Audit a public URL's CORS configuration in one API call. Inspects Access-Control headers on simple and preflight requests, detects wildcard-with-credentials, reflected origins missing Vary: Origin, null-echo origins, and over-permissive methods or headers. Returns score, grade, and recommendations.

- **URL**: https://apify.com/phoenix2810/cors-auditor.md
- **Developed by:** [Sanskar Jaiswal](https://apify.com/phoenix2810) (community)
- **Categories:** Developer tools, SEO tools, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## CORS Auditor

Audit a public URL's Cross-Origin Resource Sharing (CORS) configuration in one API call. Inspects `Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, `Access-Control-Expose-Headers`, `Access-Control-Max-Age`, and `Vary: Origin` on both a simple (GET-with-Origin) request and an OPTIONS preflight. Detects wildcard origins combined with credentials, reflected-echo servers missing `Vary: Origin`, `null` origin echo, and over-permissive method or header exposure. Returns a per-header analysis, score, letter grade, and security/devops recommendations. Built for API teams, security teams, devops engineers, and site migration QA.

### Use cases

- **API teams** - verify CORS preflight returns the right `Access-Control-Allow-Methods`, `Allow-Headers`, and a sensible `Max-Age` before shipping a new endpoint or version
- **Security teams** - catch over-permissive CORS configurations (`Access-Control-Allow-Origin: *` with credentials, `null` origin echo, reflected origins without `Vary: Origin`) that leak authenticated responses to attacker-controlled origins
- **Devops teams** - confirm preflight and simple-response CORS headers survive moves between framework versions, reverse proxies, API gateways, and edge providers
- **Site migration QA** - detect regressions where a new CDN, load balancer, or rewrite rule strips `Access-Control-*` headers or stops echoing `Vary: Origin`
- **Frontend platform teams** - debug cross-origin fetch failures and credential-not-included errors against API hosts

### Input

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `startUrl` | string | yes | - | Public URL to audit. The actor fetches the resource with an `Origin` header and performs an OPTIONS preflight. HTTP and HTTPS only. Private IP ranges are blocked. |
| `probeOrigin` | string | no | `https://probe.cors-auditor.local` | Cross-origin to send in the `Origin` header. Used to detect reflected-echo servers. Accepts `https://host[:port]` or a bare hostname. |
| `probeMethod` | string | no | `POST` | HTTP method requested via `Access-Control-Request-Method` during preflight. Use the method your API actually needs (e.g., `POST`, `PUT`, `PATCH`, `DELETE`). |
| `timeoutSeconds` | integer | no | `10` | Per-request timeout (3-30 seconds) |

#### Example input

```json
{
  "startUrl": "https://api.example.com/v1/health",
  "probeOrigin": "https://app.example.com",
  "probeMethod": "POST",
  "timeoutSeconds": 10
}
```

### Output

A single dataset item with the full audit:

| Field | Type | Description |
|---|---|---|
| `inputUrl` | string | The URL provided as input |
| `finalUrl` | string | Final URL after redirects on the simple request |
| `https` | boolean | Whether the final response was served over HTTPS |
| `status` | integer | HTTP status code of the simple (GET) request |
| `preflightStatus` | integer | null | HTTP status code of the OPTIONS preflight response, or null if no usable response was returned |
| `allowOrigin` | string | null | `Access-Control-Allow-Origin` value on the simple GET response |
| `allowOriginPreflight` | string | null | `Access-Control-Allow-Origin` value on the OPTIONS preflight response |
| `allowsCredentials` | boolean | Whether `Access-Control-Allow-Credentials: true` is present on either response |
| `isWildcard` | boolean | Whether `Access-Control-Allow-Origin` is the wildcard `*` on either response |
| `isReflected` | boolean | Whether the server reflected the probe `Origin` back instead of using a static value |
| `isNullEcho` | boolean | Whether `Access-Control-Allow-Origin` is the literal string `null` (often exploitable from sandboxed iframes and `file:` origins) |
| `allowMethods` | array | `Access-Control-Allow-Methods` values from the preflight response |
| `allowHeaders` | array | `Access-Control-Allow-Headers` values from the preflight response |
| `exposeHeaders` | array | `Access-Control-Expose-Headers` values from the simple response |
| `maxAge` | integer | null | `Access-Control-Max-Age` from the preflight response (seconds) |
| `varyOrigin` | boolean | Whether `Vary` includes `Origin` on either response (required when `Allow-Origin` is reflected) |
| `headers` | array | Per-header analysis (see below) |
| `issues` | array | Aggregated issue descriptions |
| `score` | integer | CORS readiness score (0-100) |
| `grade` | string | Letter grade (A+, A, B, C, D, E, F) |
| `checkedAt` | string | ISO 8601 timestamp |
| `recommendations` | array | Actionable recommendations |

#### `headers` array

Each entry contains:

| Field | Type | Description |
|---|---|---|
| `name` | string | Display name of the header check |
| `header` | string | Canonical check key |
| `status` | string | `good`, `warn`, `missing`, or `info` |
| `note` | string | Human-readable explanation of the current state |
| `weight` | integer | Weight of this check in the score |
| `recommendation` | string | null | Fix recommendation, or null when the check is `good` |

#### Headers checked

| Check | Header(s) | What is checked |
|---|---|---|
| Allow-Origin (simple) | `Access-Control-Allow-Origin` on GET | presence, wildcard, `null` echo, reflected-echo behavior, static-literal origins |
| Allow-Origin (preflight) | `Access-Control-Allow-Origin` on OPTIONS | presence, wildcard, `null` echo, reflected-echo behavior, static-literal origins |
| Allow-Credentials | `Access-Control-Allow-Credentials` | presence; flagged as a warning when paired with `Allow-Origin: *` (browsers reject the combination) |
| Allow-Methods | `Access-Control-Allow-Methods` | presence on preflight; warns when the requested method is not in the list and when `*` is used (non-standard) |
| Allow-Headers | `Access-Control-Allow-Headers` | presence on preflight; warns when `*` is used (non-standard for client headers) |
| Expose-Headers | `Access-Control-Expose-Headers` | presence on simple response; warns when `*` is used (non-standard) |
| Max-Age | `Access-Control-Max-Age` | presence; warns when shorter than 600 seconds (browsers preflight every request) |
| Vary: Origin | `Vary` | required when `Allow-Origin` is reflected from the request; warns when missing (CDN caches may pin one client's origin) |

#### Security-relevant detections

The audit reports the following CORS risks:

- **Wildcard with credentials** - `Access-Control-Allow-Origin: *` combined with `Access-Control-Allow-Credentials: true` is invalid per the Fetch spec; browsers ignore it and credentials fall back to same-origin.
- **Null origin echo** - `Access-Control-Allow-Origin: null` is treated as a permissive origin by browsers, attacker-controlled from sandboxed iframes, `data:` URLs, and `file:` origins.
- **Reflected origin without `Vary: Origin`** - servers that echo the request `Origin` back must include `Vary: Origin`; otherwise a CDN may serve one client's `Allow-Origin` to a different client and bypass the intended origin allow-list.
- **Over-permissive method/header wildcard** - `Access-Control-Allow-Methods: *` and `Access-Control-Allow-Headers: *` are non-standard; many browsers ignore the wildcard for client-initiated header lists.
- **Unlisted preflight method** - when `probeMethod` requests a method that is not in `Access-Control-Allow-Methods`, the preflight will be rejected by the browser.

#### Grading scale

| Score range | Grade |
|---|---|
| 95-100 | A+ |
| 85-94 | A |
| 75-84 | B |
| 65-74 | C |
| 50-64 | D |
| 30-49 | E |
| 0-29 | F |

#### Example output

```json
{
  "inputUrl": "https://api.example.com/v1/health",
  "finalUrl": "https://api.example.com/v1/health",
  "https": true,
  "status": 200,
  "preflightStatus": 204,
  "allowOrigin": "https://app.example.com",
  "allowOriginPreflight": "https://app.example.com",
  "allowsCredentials": true,
  "isWildcard": false,
  "isReflected": false,
  "isNullEcho": false,
  "allowMethods": ["get", "post", "put", "delete"],
  "allowHeaders": ["authorization", "content-type"],
  "exposeHeaders": ["x-trace-id"],
  "maxAge": 86400,
  "varyOrigin": true,
  "headers": [
    {
      "name": "Access-Control-Allow-Origin (simple)",
      "header": "allow-origin-simple",
      "status": "good",
      "note": "Access-Control-Allow-Origin: https://app.example.com (static origin) on the simple response.",
      "weight": 25,
      "recommendation": null
    },
    {
      "name": "Access-Control-Allow-Origin (preflight)",
      "header": "allow-origin-preflight",
      "status": "good",
      "note": "Access-Control-Allow-Origin: https://app.example.com (static origin) on the preflight response.",
      "weight": 20,
      "recommendation": null
    }
  ],
  "issues": [],
  "score": 95,
  "grade": "A+",
  "checkedAt": "2026-08-10T12:00:00.000Z",
  "recommendations": [
    "CORS configuration looks consistent and well-scoped. Re-run after API deploys or origin changes to catch regressions."
  ]
}
```

### Security

- Only public HTTP/HTTPS URLs are accepted
- SSRF protection: localhost, private IPv4/IPv6, and DNS-resolving-to-private IPs are blocked
- URLs with embedded credentials are rejected
- Redirects are manually revalidated before following (max 3)
- No browser automation, no cookies stored, no body retained beyond draining
- `probeOrigin` is validated and stripped to its scheme/host/port; it is used only as the `Origin` request header, never as a target

### Pricing

Pay per event:

| Event | Price |
|---|---|
| Actor start | $0.005 |
| URL audited | $0.01 |

A single URL audit (one simple GET plus one OPTIONS preflight) costs approximately $0.015.

### FAQ

**How is this different from the HTTP Security Headers Auditor?**
The HTTP Security Headers Auditor checks security response headers such as `Strict-Transport-Security`, `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, and `Referrer-Policy`. This actor focuses exclusively on Cross-Origin Resource Sharing headers (`Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, `Access-Control-Expose-Headers`, `Access-Control-Max-Age`) and `Vary: Origin`, evaluated against both a simple GET and an OPTIONS preflight.

**Why does the actor perform two requests?**
Browsers enforce CORS differently for simple requests (GET, some POST forms) and preflighted requests (anything with custom headers or non-simple methods). A simple GET with `Origin` reveals the runtime CORS posture; an OPTIONS preflight with `Access-Control-Request-Method` reveals the policy the server advertises to browsers. Comparing both catches common misconfigurations where the API answers preflight correctly but returns a different `Allow-Origin` on the actual response.

**How should I set `probeMethod`?**
Use the HTTP method your frontend actually calls the API with. The actor sends it via `Access-Control-Request-Method` on the preflight and verifies `Access-Control-Allow-Methods` includes it. `POST` covers most JSON APIs; use `PATCH`, `PUT`, or `DELETE` for REST endpoints that mutate state.

**Why is `Access-Control-Allow-Origin: null` flagged?**
Some servers return `null` when an unrecognized `Origin` is sent (for example, behind a proxy that strips unknown origins). Browsers treat `null` as a specific origin that sandboxed iframes, `data:` URLs, and `file:` pages can produce, which means attacker-controlled pages may be able to read authenticated responses. Return a concrete trusted origin or omit `Allow-Origin` for disallowed origins.

**What is reflected-echo and why does it need `Vary: Origin`?**
Many servers validate the request `Origin` against an allow-list and, when allowed, return `Access-Control-Allow-Origin: <request origin>` (an "echo"). When `Vary: Origin` is omitted, a shared CDN cache may store the response with one client's `Allow-Origin` value and serve it to a different client whose `Origin` is not on the allow-list. The audit flags this configuration as a warning.

**Does the actor follow redirects?**
Yes, up to 3 redirects. Each redirect target is revalidated for SSRF safety before it is followed. CORS headers are evaluated on the final response.

**Can I audit static assets (CSS, JS, fonts)?**
Yes. The behavior is identical. Static assets intended for cross-origin use should return `Access-Control-Allow-Origin: *` (without credentials); assets that must be authenticated should return a specific origin plus `Vary: Origin`.

# Actor input Schema

## `startUrl` (type: `string`):

Public URL to audit. The actor performs a normal GET with an Origin header and an OPTIONS preflight with a sample cross-origin request. HTTP and HTTPS only. Private IP ranges are blocked.

## `probeOrigin` (type: `string`):

Cross-origin to send in the Origin header (defaults to https://probe.cors-auditor.local so we can detect reflected-echo servers). Must be a public http(s) URL or bare hostname.

## `probeMethod` (type: `string`):

HTTP method to request in Access-Control-Request-Method during the OPTIONS preflight. Use the method your API actually needs; most APIs need POST, PUT, PATCH, or DELETE.

## `timeoutSeconds` (type: `integer`):

Timeout for each HTTP request.

## Actor input object example

```json
{
  "startUrl": "https://example.com",
  "probeOrigin": "https://probe.cors-auditor.local",
  "probeMethod": "POST",
  "timeoutSeconds": 10
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrl": "https://example.com",
    "probeOrigin": "https://probe.cors-auditor.local",
    "probeMethod": "POST"
};

// Run the Actor and wait for it to finish
const run = await client.actor("phoenix2810/cors-auditor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrl": "https://example.com",
    "probeOrigin": "https://probe.cors-auditor.local",
    "probeMethod": "POST",
}

# Run the Actor and wait for it to finish
run = client.actor("phoenix2810/cors-auditor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrl": "https://example.com",
  "probeOrigin": "https://probe.cors-auditor.local",
  "probeMethod": "POST"
}' |
apify call phoenix2810/cors-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,phoenix2810/cors-auditor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/6qvuzh6BNdxA3gZD7/builds/dx6ot0232OskGUYnt/openapi.json
