Skip to content

CORS Tester

What the CORS Tester sends from the Tamperbox server, how it replays the browser's CORS algorithm on the preflight and the actual response, what each step means, and how to fix and verify each block.

Open Markdown

The tool page is tamperbox.dev/cors-tester. It opens with https://tamperbox.dev/ in the box and nothing sent; you name the calling page’s origin and press Send preflight. This page is the long version: what the server sends, every step the tool replays from the browser’s algorithm, what each outcome means, and how to fix and verify each one.

Where processing happens: pressing Send preflight sends the URL, origin, method and header names to the Tamperbox server, which sends one OPTIONS request and, for GET and HEAD only, one request carrying just the Origin header, then returns the status and headers of each. Pasted response headers are read in your browser and never uploaded. No cookies, no header values and no request bodies are ever sent. Nothing is stored.

Four questions, in the order a browser asks them:

  1. Does this request need a preflight? A method or header outside the safelist makes the browser ask first.
  2. Did the preflight permit it? A 2xx status, the origin allowed, credentials allowed when cookies are sent, the method and every custom header listed.
  3. Can the page read the real response? The origin and credentials rules again, on the response to the actual request.
  4. Will caches keep it that way? Vary: Origin when a specific origin is echoed, and how long the preflight is cached.

The verdict names one of three outcomes. Blocked names the step that failed. Allowed means every step passed and an actual response was checked. Not decided means nothing failed but the exchange is incomplete: the real request was not sent because the method could change data, or a pasted exchange lacks one of its two halves.

The box takes the address the page would call. parseUrl in site-ui.js reads it: an address without a scheme gets https://; spaces, non-HTTP schemes, a user name or password, a port other than 80 or 443, an IP address and a host without a top-level domain are refused with a message, and the port and credential cases offer a repair. The fragment is dropped.

Pasted text is tidied by the shared cleaner (cleanInput in lib/input-clean.js, applied through tidySource): a quoted or bracketed address loses its wrapping and trailing punctuation, and the hint says what was kept. Because this is a page tool, the whole address stays, not just the host. Text that fits nothing stays as pasted so the parser’s message explains it. Typing is never rewritten; a break or repair load is never cleaned.

A real server is never contacted on its own. The page shows “Ready to test” and waits for Enter or the Send preflight button. The reserved harbor-supply.example hosts answer from a local fixture at once, without a network request.

Field What the tool does with it
Calling page origin Must be a scheme and host, optionally a port, with no path, query or trailing slash; null is accepted as the opaque origin. Sent as Origin.
Method GET, HEAD, POST, PUT, PATCH or DELETE. Sent as Access-Control-Request-Method. Only GET and HEAD are ever sent as the actual request.
Request headers Names only, split on commas or whitespace, lowercased, deduplicated and sorted; at most 20 valid token names. Sent as Access-Control-Request-Headers. Values are never asked for.
Send cookies A flag that switches on the credentials rules. No cookie is ever sent.
Single pasted response is Preflight or Actual, used only when one pasted block is supplied.

Changing the origin or headers re-checks pasted headers and fixture endpoints at once; a real server waits for Enter or the button again. When the box holds a real address, the fixture’s origin https://www.harbor-supply.example is cleared, so a third party’s server is never probed with the sample origin.

Text that starts with a status line or name: value lines is read as responses by parseResponses in http-checks.js: each HTTP/x 2xx line starts a block, header names are lowercased, and an indented continuation line joins the header above it. One block is judged in the role you select, with an assumed status of 204 for a preflight and 200 for an actual response when no status line was pasted. Two blocks are the preflight first and the actual response second. Nothing in a paste leaves the browser.

The verdict line is Blocked, Allowed or Not decided, followed by the failing step’s title or a sentence naming the origin, host and request. The chips are buttons: “Blocked at step n”, “Preflight needed” or “No preflight”, and “n checks passed”, each scrolling to the steps.

Findings repeat the steps that failed or warned, worst first; passes fold into one line.

One row per step, in the order the browser applies them, each with a badge and, where it applies, “Browser sends” beside “Server answers” with the exact header lines. After a block, a greyed row says what never happens: the request is not sent, or the page cannot read the response. After a passing preflight for a method the tool does not send, a greyed row says the request itself was not sent.

When a step blocks the request, the code window holds the response headers that would allow it, built by corsFix from what the server already sent, in the syntax of the server you pick: nginx, Apache, Caddy, a _headers file, IIS web.config or Express. The note under it says where the lines go and whether they belong on the OPTIONS answer or on the actual response.

The raw exchange: for a live check, the OPTIONS request and the GET or HEAD request as our server sent them, with each answer’s status, time and headers, the CORS headers marked. For a paste, the blocks as pasted.

Break it and Heal it log what they replaced. Revert undoes the newest change and Reset input returns to what you entered, including the request fields a break changed.

Take it further carries a value derived from this result into another tool in the same category: the Redirect Checker at the request URL when the preflight was redirected, the preflight as a curl command into the Postman Collection Converter, and the HTTP/3 and Compression Checker at the API origin. Each opens the other tool with the value in its box; nothing is put in a URL.

The evaluation is evaluateCors in public/lib/http-checks.js, run in the browser on the preflight and actual responses, whether they came from our server or from a paste. It stops at the first failure, as a browser does.

A request is sent directly when its method is GET, HEAD or POST and every request header is one of accept, accept-language or content-language. Anything else triggers a preflight. This is stricter than the Fetch Standard’s CORS-safelisted request-header rule, which also safelists Content-Type with the values application/x-www-form-urlencoded, multipart/form-data and text/plain, and Range with a simple range. The tool asks for header names only, never values, so it cannot tell a form body from a JSON body and treats Content-Type as custom; a JSON body is the usual case, and the stricter reading shows the preflight such a request will trigger. The CORS-safelisted method list is applied as the standard writes it.

A preflight must get a 2xx status. In the CORS-preflight fetch the standard says: “If response’s status is not an ok status, then return a network error.” The tool reports three failures:

Outcome Finding Rule
No response Preflight got no answer The OPTIONS request failed at DNS, connect or timeout; the stage is named
3xx or any Location Preflight was redirected (status) Fetch sets the preflight’s redirect mode to error, so a redirect is a network error
Other non-2xx Preflight answered (status) An ok status is 200 to 299; 404 and 405 are the common framework answers

Our server sends the OPTIONS with redirect: manual, so a redirect is seen, not followed, and the Location is shown.

The CORS check decides this step, on the preflight and again on the actual response. The standard: “If origin is null, then return failure”, “If origin is * and credentials mode is not include, then return success”, “If request’s origin, serialized and UTF-8 encoded, is not origin, then return failure”, “If credentials mode is not include, then return success”, “If allowCredentials is not true, then return failure”. The tool’s readings, in order: no header is a block; more than one header or a comma-separated value is a block (the header holds exactly one origin or *); * with cookies is a block; * without cookies passes; any other value must equal the calling origin byte for byte, as RFC 6454 section 7 serializes an origin, which has no trailing slash.

Only with Send cookies on. Access-Control-Allow-Credentials must be exactly true, lowercase; anything else, including a missing header, is a block.

Checked on the preflight only, and only for the parts that made it necessary. The method must appear in Access-Control-Allow-Methods exactly as written, or the list must contain * and the request must be without credentials. Each custom header must appear in Access-Control-Allow-Headers, compared case-insensitively, or * must be present without credentials; * never covers Authorization, which must be named. Both follow the wildcard rules in the CORS-preflight fetch: “If request’s credentials mode is not include and methods contains *” and the matching sentence for headers, with the Authorization exception stated there.

Access-Control-Max-Age is information, never a block. Without it the tool says browsers cache the answer for 5 seconds, the default the standard sets for a max-age value that is absent; Chrome caps any value at 7,200 seconds, a browser limit, not a standard one.

Our server sends the actual request only for GET and HEAD, and only when no preflight was needed or the preflight permitted the request as judged by the same rules on the server. It carries the Origin header and nothing else that identifies you. A status below 400 passes; 4xx and 5xx are noted, not failed, because CORS can allow a page to read an error. The origin and credentials rules run again. Then, when a specific origin is echoed without Vary: Origin (RFC 9110 section 12.5.5), a warning says a shared cache can serve one origin’s answer to another, as RFC 9111 section 4.1 describes the use of Vary in selecting a stored response. Access-Control-Expose-Headers is listed as information.

The result is Allowed only when no step failed, the preflight was present whenever it was needed, and an actual response was checked. POST, PUT, PATCH and DELETE are never sent because they could change data on a server you may not own, so a passing preflight for them ends in Not decided.

The tool is an observer of one exchange, not a conforming user agent. It reproduces the browser’s decision procedure on the responses it has and names where it is stricter or stops.

The CORS protocol of the WHATWG Fetch Living Standard is the current specification; the W3C Cross-Origin Resource Sharing Recommendation of 2014 is obsolete and superseded by it, and is cited here only as history. Implemented in the browser from the responses: the preflight ok-status and redirect rules, the CORS check, the wildcard rules for methods and headers with the Authorization exception, and the max-age default. Implemented on our server: the decision to send the actual request only after a preflight that passes the same rules. Stricter than the standard: Content-Type and Range always count as custom headers, because the tool knows names, not values. Not implemented: the Access-Control-Allow-Private-Network check, the CORS-safelisted response-header filtering beyond listing Access-Control-Expose-Headers, and the preflight cache itself.

RFC 6454 is a Proposed Standard. Section 7 defines the serialization the tool compares against: scheme, host and port, no path. The page refuses an origin with a path, query or trailing slash before anything is sent, and accepts the string null as the opaque origin.

RFC 9110, HTTP Semantics, is an Internet Standard that replaces the earlier RFC 7230 family of HTTP semantics documents. Used: section 9.3.7 for OPTIONS, section 15.4 for the 3xx class the tool treats as a redirect on a preflight, and section 12.5.5 for Vary. RFC 9111, HTTP Caching, section 4.1, is behind the Vary: Origin warning, which is a cache-compatibility warning, not evidence that any cache misbehaved.

Each row is a step the code can emit, with its id in evaluateCors. Ids beginning with preflight- and actual- are the same rule applied to each response.

Finding Id Severity Standard Meaning for the page Fix
Preflight got no answer preflight-status Fail Fetch, CORS-preflight fetch The OPTIONS never got a response from this vantage point Check DNS, the firewall and that OPTIONS reaches the application
Preflight was redirected (status) preflight-status Fail Fetch, redirect mode error The browser stops; the request is never sent Answer OPTIONS at the requested URL itself, before any redirect rule
Preflight answered (status) preflight-status Fail Fetch, ok status Usually no route handles OPTIONS, or auth middleware rejected it Route OPTIONS to a handler that answers 204 before authentication
Preflight allows no origin preflight-origin Fail Fetch, CORS check No Access-Control-Allow-Origin Send the calling origin or *
More than one allowed origin preflight-origin Fail Fetch, CORS check A list is not a valid value Echo the one origin that asked, with Vary: Origin
Wildcard origin with credentials preflight-origin Fail Fetch, CORS check * is refused when cookies are sent Echo the exact origin and send Access-Control-Allow-Credentials: true
Origin (x) is not the one allowed preflight-origin Fail RFC 6454 section 7 The server allows a different origin, or the same one with a slash Match scheme, host and port exactly
Credentials not allowed preflight-credentials Fail Fetch, CORS check With cookies, true is required Send Access-Control-Allow-Credentials: true
(Method) not allowed methods Fail Fetch, preflight wildcard rule Method missing from the list, or * with credentials List the method by name; names are case-sensitive
(Header) not allowed headers Fail Fetch, preflight wildcard rule Header missing, or Authorization under * Add each name to Access-Control-Allow-Headers

Blocks and warnings at the actual response

Section titled “Blocks and warnings at the actual response”
Finding Id Severity Standard Meaning for the page Fix
Request got no answer actual-status Fail None The GET or HEAD failed from this vantage point Check reachability; a preflight alone cannot allow the call
Response (status) actual-status Pass or Info None Below 400 passes; an error is still readable if CORS allows it None for CORS; fix the application error separately
Response allows no origin, More than one, Wildcard with credentials, Origin not the one allowed actual-origin Fail Fetch, CORS check The same origin rules, on the response the page reads Send the origin headers on the actual response too, not only on OPTIONS
Credentials not allowed actual-credentials Fail Fetch, CORS check Same as the preflight rule Same as above
No Vary: Origin vary Check RFC 9110 section 12.5.5, RFC 9111 section 4.1 A shared cache can hand one origin’s answer to another Add Vary: Origin whenever a specific origin is echoed
Finding Id Severity Meaning
Preflight required needed Info Names the method or headers that make the browser ask first
No preflight needed needed Info A simple request; the response alone decides
Preflight not supplied preflight-missing Info A paste without the OPTIONS block; the actual response is still checked
Preflight cached for n s, Preflight not cached max-age Info How long browsers reuse the answer; 5 seconds without the header
Extra headers readable expose Info The headers Access-Control-Expose-Headers lets the page read
Any origin allowed (*) preflight-origin, actual-origin Pass Fine for public data; cookies would need the exact origin

The framework has no route for OPTIONS, or an authentication layer runs before the CORS handler and rejects a request that carries no token. Browsers never send credentials with a preflight, so the handler must answer before authentication. Route OPTIONS to a handler that returns 204 with the allow headers. The tool’s code window gives the lines for your server; for nginx it is one add_header … always; line per header in the server block, where always keeps the headers on error responses too. Confirm independently:

curl -i -X OPTIONS 'https://api.example.com/v1/items' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: content-type'

The status line must be 2xx and the Access-Control-Allow-* lines must be present. Then press Send preflight again.

A rule that adds https://, a trailing slash or www runs before CORS, or a sign-in redirect catches OPTIONS. The browser treats any redirect on a preflight as a network error. Make the page call the final address, and make OPTIONS answer at that address without a redirect. Take it further hands the request URL to the Redirect Checker so you can see the chain the preflight hit.

Access-Control-Allow-Origin: * is refused the moment the request carries credentials. Echo the requesting origin, add Access-Control-Allow-Credentials: true, and add Vary: Origin. Decide first whether the endpoint needs cookies at all; a bearer token in a header is a custom header, not credentials, and works with * only if Authorization is named in Access-Control-Allow-Headers.

The commonest block. The server lists Content-Type but the page also sends Authorization or X-Request-Id. Add every name the page sends; * covers all of them only without credentials and never covers Authorization. The code window merges what the server already sends with what the request needs, so pasting it does not drop existing names.

Not a block today, but a CDN or shared proxy may cache the response for one origin and serve it to another, which then fails the origin check. Add Vary: Origin wherever a specific origin is echoed.

Break it What it loads What it shows
Send cookies open.harbor-supply.example with credentials on Wildcard origin with credentials
Another origin Origin https://other-site.example Origin not the one allowed
PUT with JSON PUT with Content-Type PUT not allowed on an endpoint that lists GET and POST
OPTIONS not handled legacy-api.harbor-supply.example, PUT with Content-Type Preflight answered 405
Preflight redirected moved-api.harbor-supply.example Preflight was redirected (301)
No preflight needed GET with no headers A simple request judged on its response alone

Breaks that change only the request keep your own URL when you entered one; the two fixture-only cases load the reserved host. A break never probes a real server with the fixture’s origin. For a pasted exchange that is blocked, Heal it offers “Add the headers that allow it”: it adds the proposed headers to each pasted block in its own role, keeps the status codes, and checks again. Method, header and max-age permissions go on the preflight; origin and credentials go on both.

The page opens with https://tamperbox.dev/ loaded and the sample origin cleared, so nothing is sent until you enter the origin of your own page and press Send preflight. Live evidence changes, so this page does not record what that server answers; read the steps as they come back.

The reserved fixture https://api.harbor-supply.example/v1/stock with origin https://www.harbor-supply.example, GET and the Authorization header, is synthetic sample data answered locally. Its policy allows the shop’s own origins with credentials, lists GET and POST, allows only Content-Type and caches the preflight for 600 seconds. Read end to end: step 1, a preflight is required because of Authorization; step 2, the preflight answers 204; step 3, the origin matches exactly; step 4, Authorization not allowed, Blocked, and the greyed row says the GET is never sent. The code window proposes Access-Control-Allow-Origin: https://www.harbor-supply.example, Access-Control-Allow-Headers: Content-Type, Authorization, Vary: Origin and Access-Control-Max-Age: 600.

The Tamperbox MCP server exposes two MCP tools: use search to inspect the catalog and schema, then use execute to call POST /v1/tools/cors_tester through tamperbox.request. Supply url and origin; optionally set method, headers (up to 20 unique header names) and credentials. The credentials flag only switches on the credentials rules; no cookie is sent. The result carries the preflight and actual responses with their status, headers and timing, and actualSkipped says why no actual request was sent (method or preflight_failed), plus warnings and provenance. Keep these warnings with any conclusion:

  • The exchange was observed from the Tamperbox server, not from a user’s browser. A server can answer differently by address, cookie or firewall.
  • POST, PUT, PATCH and DELETE are never sent; a passing preflight for them is Not decided.
  • Content-Type is treated as a custom header; a real form body might not trigger a preflight.

See Connect an MCP client and Code mode for the calling pattern, and Security and privacy for where the URL and origin travel.

  • Our server is one client in one place. Address-based rules, geographic rules, cookies and firewalls can make a browser’s exchange differ from this one.
  • Only OPTIONS and, when permitted, GET or HEAD are sent. No request body, no cookie, no header value, at most 20 header names.
  • Header names are known; values are not. Content-Type therefore always counts as custom.
  • A pasted block is judged in the role you give it; the tool does not infer whether headers came from OPTIONS or from the real request.
  • Allowed needs a checked actual response. A preflight alone is never Allowed.
  • The preflight cache and private-network access checks are not modelled.
  • Nothing is stored.