# Browser API CORS Allowlist and Credential Exposure Gate (`kingii98/browser-api-cors-allowlist-and-credential-exposure-gate`) Actor

Sends a real OPTIONS preflight and a real cross-origin GET from each origin that you name, against each API path that you name. Reports which origins the API accepts, which accept credentials, and every violation class: wildcard with credentials, origin r

- **URL**: https://apify.com/kingii98/browser-api-cors-allowlist-and-credential-exposure-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 cors gate run starts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## 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

## Browser API CORS Allowlist and Credential Exposure Gate

Send a **real preflight request** and a **real cross-origin request** from each origin that you
name, against each API path that you name. The report says which origins your API accepts, which
of them may send credentials, and what changed since the last run.

Every incumbent in the store grades response headers on one GET request. This Actor sends an
OPTIONS preflight from a named origin, so it finds the two faults that a header grade cannot see:
an API that copies any origin into `Access-Control-Allow-Origin` while it allows credentials, and
an API that accepts an origin which you never wanted.

HTTP only. No browser, no proxy and no external database.

### What the Actor sends

For each origin and path pair:

1. `OPTIONS <path>` with `Origin`, `Access-Control-Request-Method` and
   `Access-Control-Request-Headers`. This is the preflight that a browser sends.
2. `GET <path>` with `Origin`. This is the real cross-origin request.

The Actor **never** sends a state-changing request. `POST`, `PUT` and `DELETE` are only *named* in
the preflight and compared against `Access-Control-Allow-Methods`. The response body is never read;
only the headers carry the CORS policy.

When a path reflects the sent origin, the Actor sends **two more calls** from a control origin
(`https://cors-probe-reflection-check.example.net`). That pair separates a real origin reflection
from a static wildcard and from a static allow list that happens to hold your origin.

### Input

Run it with empty input to see the default fixture against a stable public API. That target
answers with `Access-Control-Allow-Origin: *`, so the fixture run ends with a **failed gate** and
one `deny_list_origin_accepted` record for each deny-list origin. The run itself succeeds: a gate
verdict is a result, never a failed run.

| Field | Default | Meaning |
| --- | --- | --- |
| `apiBaseUrl` | `https://api.github.com` | The base URL of an API that you own or may test. Private, loopback and reserved addresses are refused. |
| `paths` | `["/", "/rate_limit"]` | 1 to 25 endpoint paths. |
| `expectedAllowOrigins` | `["https://app.example.com"]` | Origins that must be accepted. |
| `expectedDenyOrigins` | `["https://evil.example.com", "null", "http://attacker.example"]` | Origins that must be refused. |
| `includeSubdomainProbe` | `true` | Add `https://cors-probe.<your domain>` to the deny list. |
| `methods` | `["GET", "POST", "PUT", "DELETE"]` | Methods to request in the preflight. The first one is sent in `Access-Control-Request-Method`. |
| `requestHeaders` | `["authorization", "content-type"]` | Headers to request in the preflight. |
| `authorizationStatement` | see schema | Free text. You confirm ownership. The Actor writes it to the run log and to the summary. |
| `trackMatrix` | `true` | Keep the verdict matrix in the key-value store and report the diff. |
| `matrixKey` | `""` | Empty means: derive the key from the base URL. |
| `timeoutSeconds` | `15` | Timeout of one request. |
| `maxConcurrency` | `5` | Pairs probed at the same time. |
| `maxRequests` | `900` | Hard bound on the run. |
| `maxRedirects` | `0` | A browser preflight does not follow a redirect. Each hop that the Actor follows takes the public-address test again. |

An origin that stands in both lists is treated as an allow-list origin.

### Output

The dataset holds four record types.

**`probe`** — one row for each origin and path pair: `origin`, `path`, `method`,
`preflightStatus`, `actualStatus`, `allowOrigin`, `reflectsOrigin`, `allowCredentials`,
`allowedMethods`, `allowedHeaders`, `missingMethods`, `missingHeaders`, `varyOrigin`, `maxAge`,
`accepted`, `changeSinceLastRun` and a `verdict` of `expected_allow_ok`, `expected_deny_ok`,
`unexpected_allow` or `unexpected_deny`.

**`violation`** — the violation list. Each item carries a named class:

| Class | Severity | Meaning |
| --- | --- | --- |
| `origin_reflection_with_credentials` | critical | The API copies the sent origin and allows credentials. A control origin was reflected too, so any site can read the answer of a signed-in user. |
| `wildcard_with_credentials` | high | `Access-Control-Allow-Origin: *` together with `Access-Control-Allow-Credentials: true`. A browser refuses that pair, so your own credentialed call fails. |
| `null_origin_accepted` | high | The API accepts the origin value `null`, which a sandboxed frame sends. |
| `deny_list_origin_accepted` | high | An origin of your deny list is accepted. |
| `allow_list_origin_refused` | medium | An origin of your allow list is refused, so your application breaks. |
| `missing_vary_origin` | low | An origin-dependent `Access-Control-Allow-Origin` without `Vary: Origin`. A shared cache can then give one origin the header of another. |

The three classes that describe the endpoint itself (`wildcard_with_credentials`,
`origin_reflection_with_credentials`, `missing_vary_origin`) are written once for each path. The
three classes that describe one pair are written for each pair.

**`change`** — the diff against the last run: `became_accepted`, `became_refused`,
`credentials_enabled` or `credentials_disabled`. The first run writes the matrix and reports no
diff.

**`summary`** — one record with `gatePass`, the counts, `status`, `note` and your
`authorizationStatement`. The gate passes when there is no violation, no failed pair and no
skipped pair.

A business verdict is never a failed run. A failed gate, a refused target, an unreachable API and
zero violations all end with a **SUCCEEDED** run, a dataset record and a status message. Only a
malfunction, for example an input that cannot be parsed, gives a failed run.

### Pay per event

| Event | Unit | Price |
| --- | --- | --- |
| `cors-gate-run-start` | one run | USD 0.01 |
| `origin-endpoint-probed` | one origin and path pair, preflight plus actual request | USD 0.003 |
| `violation-recorded` | one confirmed violation in the violation list | USD 0.03 |

A pair that was skipped by the request bound, and a pair that never reached the API, are not
charged. A matrix of 8 origins by 25 paths is 200 pairs, which is USD 0.60 plus the run start and
the violations.

### Repeat use

Run it once after each deployment of the API or of the gateway, through your build pipeline, and
once a week as a regression run. CORS policy lives in gateway configuration, so it drifts silently
at each infrastructure change. The `change` records name the drift.

### Authorization

The Actor sends requests to a live API. Run it only against an API that you own, or that you are
authorized to test. State that in `authorizationStatement`; the Actor keeps the text with the
report.

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `apiBaseUrl` (type: `string`):

The base URL of an API that you own, or that you are authorized to test. The Actor joins each path to this base URL. Private, loopback and reserved addresses are refused.

## `paths` (type: `array`):

1 to 25 endpoint paths, for example /v1/account and /v1/orders. Every path is probed from every origin.

## `expectedAllowOrigins` (type: `array`):

The origins of your own browser applications, for example https://app.example.com. An origin of this list that the API refuses gives the violation class allow\_list\_origin\_refused.

## `expectedDenyOrigins` (type: `array`):

The origins that the API must never accept. The value null is a real origin value that a sandboxed frame sends. An origin of this list that the API accepts gives the violation class deny\_list\_origin\_accepted.

## `includeSubdomainProbe` (type: `boolean`):

Add https://cors-probe.<your domain> to the deny list. A gateway that matches the host with a loose suffix rule accepts that origin, and an attacker who takes one subdomain then reads your API.

## `methods` (type: `array`):

The methods that your application uses. The first method is named in Access-Control-Request-Method. Every method is compared against Access-Control-Allow-Methods. The Actor itself sends only OPTIONS and GET, so it never changes data.

## `requestHeaders` (type: `array`):

The header names that your application sends. They are named in Access-Control-Request-Headers and compared against Access-Control-Allow-Headers.

## `authorizationStatement` (type: `string`):

Free text. Confirm that you own the target, or that you are authorized to test it. The Actor writes this text to the run log and to the summary record.

## `trackMatrix` (type: `boolean`):

Store the verdict of each origin and path pair in the key-value store, and report every change in the next run. The first run writes the matrix and reports no diff.

## `matrixKey` (type: `string`):

Leave this empty to derive the record key from the base URL. Set it to keep more than one matrix for the same API, for example one for staging and one for production.

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

The Actor stops one request after this time.

## `maxConcurrency` (type: `integer`):

How many origin and path pairs the Actor probes at the same time.

## `maxRequests` (type: `integer`):

A hard bound on the run. Each pair costs two requests, and each confirmed reflection costs two more. A pair above the bound is reported as skipped.

## `maxRedirects` (type: `integer`):

Zero is the right value for a CORS test, because a browser preflight does not follow a redirect. The Actor tests each redirect hop against the public-address guard, and it never follows more hops than this number.

## Actor input object example

```json
{
  "apiBaseUrl": "https://api.github.com",
  "paths": [
    "/",
    "/rate_limit"
  ],
  "expectedAllowOrigins": [
    "https://app.example.com"
  ],
  "expectedDenyOrigins": [
    "https://evil.example.com",
    "null",
    "http://attacker.example"
  ],
  "includeSubdomainProbe": true,
  "methods": [
    "GET",
    "POST",
    "PUT",
    "DELETE"
  ],
  "requestHeaders": [
    "authorization",
    "content-type"
  ],
  "authorizationStatement": "I own this API, or I am authorized to test it. The Actor sends only OPTIONS and GET requests.",
  "trackMatrix": true,
  "matrixKey": "",
  "timeoutSeconds": 15,
  "maxConcurrency": 5,
  "maxRequests": 900,
  "maxRedirects": 0
}
```

# Actor output Schema

## `dataset` (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 = {
    "apiBaseUrl": "https://api.github.com",
    "paths": [
        "/",
        "/rate_limit"
    ],
    "expectedAllowOrigins": [
        "https://app.example.com"
    ],
    "expectedDenyOrigins": [
        "https://evil.example.com",
        "null",
        "http://attacker.example"
    ],
    "includeSubdomainProbe": true,
    "methods": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
    ],
    "requestHeaders": [
        "authorization",
        "content-type"
    ],
    "authorizationStatement": "I own this API, or I am authorized to test it. The Actor sends only OPTIONS and GET requests.",
    "trackMatrix": true,
    "matrixKey": "",
    "timeoutSeconds": 15,
    "maxConcurrency": 5,
    "maxRequests": 900,
    "maxRedirects": 0
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/browser-api-cors-allowlist-and-credential-exposure-gate").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 = {
    "apiBaseUrl": "https://api.github.com",
    "paths": [
        "/",
        "/rate_limit",
    ],
    "expectedAllowOrigins": ["https://app.example.com"],
    "expectedDenyOrigins": [
        "https://evil.example.com",
        "null",
        "http://attacker.example",
    ],
    "includeSubdomainProbe": True,
    "methods": [
        "GET",
        "POST",
        "PUT",
        "DELETE",
    ],
    "requestHeaders": [
        "authorization",
        "content-type",
    ],
    "authorizationStatement": "I own this API, or I am authorized to test it. The Actor sends only OPTIONS and GET requests.",
    "trackMatrix": True,
    "matrixKey": "",
    "timeoutSeconds": 15,
    "maxConcurrency": 5,
    "maxRequests": 900,
    "maxRedirects": 0,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/browser-api-cors-allowlist-and-credential-exposure-gate").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 '{
  "apiBaseUrl": "https://api.github.com",
  "paths": [
    "/",
    "/rate_limit"
  ],
  "expectedAllowOrigins": [
    "https://app.example.com"
  ],
  "expectedDenyOrigins": [
    "https://evil.example.com",
    "null",
    "http://attacker.example"
  ],
  "includeSubdomainProbe": true,
  "methods": [
    "GET",
    "POST",
    "PUT",
    "DELETE"
  ],
  "requestHeaders": [
    "authorization",
    "content-type"
  ],
  "authorizationStatement": "I own this API, or I am authorized to test it. The Actor sends only OPTIONS and GET requests.",
  "trackMatrix": true,
  "matrixKey": "",
  "timeoutSeconds": 15,
  "maxConcurrency": 5,
  "maxRequests": 900,
  "maxRedirects": 0
}' |
apify call kingii98/browser-api-cors-allowlist-and-credential-exposure-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/browser-api-cors-allowlist-and-credential-exposure-gate"
        }
    }
}

```

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/hp1khKHFPnW6beDMM/builds/lLWHBCyMW5uni2CbP/openapi.json
