cors-auditor
Audit a site's Cross-Origin Resource Sharing (CORS) configuration for misconfigurations — wildcard origin with credentials, reflected arbitrary Origin, the 'null' origin, overly broad allowed methods, and risky credentialed CORS. Use when the user asks to "check my CORS config",
Install
npx skills add https://github.com/NovaCode37/claude-security-skills/tree/main/skills/cors-auditor
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install novacode37-claude-security-skills@llmmart
git clone https://github.com/NovaCode37/claude-security-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole novacode37/claude-security-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
CORS Misconfiguration Auditor
Inspects CORS response headers and reports exploitable misconfigurations with
fixes. The analysis core is pure and offline-testable; live probing uses only
stdlib urllib and sends a throwaway Origin header to detect origin
reflection.
When to use this skill
- "Audit the CORS configuration of https://api.example.com."
- "Is it safe that my API returns Access-Control-Allow-Origin: *?"
- "Does my server reflect any Origin back?"
What it checks
- Wildcard + credentials —
Allow-Origin: *withAllow-Credentials: true(cors-wildcard-credentials). - Reflected origin — server echoes the request Origin back
(
cors-reflected-origin); critical when combined with credentials. - Null origin —
Allow-Origin: null(cors-null-origin). - Wildcard origin —
Allow-Origin: *without credentials (cors-wildcard). - Credentialed CORS — informational note when credentials are enabled
(
cors-credentials-enabled). - Wildcard methods —
Allow-Methods: *(cors-methods-wildcard).
How to run it
# Live probe (sends a throwaway Origin to test reflection)
python skills/cors-auditor/auditor.py https://api.example.com
# Probe with a specific origin
python skills/cors-auditor/auditor.py https://api.example.com --origin https://evil.example
# Offline: audit a captured header block; pass --origin to test reflection
python skills/cors-auditor/auditor.py --headers-file resp.txt --origin https://evil.example
# Only fail CI on high/critical (hides the medium wildcard + info notes)
python skills/cors-auditor/auditor.py https://api.example.com --min-severity high
Exit codes: 0 clean · 1 findings reported · 2 fetch/usage error.
Every reported finding fails the build. The default reports everything down to
info (including cors-credentials-enabled); raise --min-severity to
low/medium/high to filter advisory notes out of both the report and the
exit code.
Recommended workflow for Claude
- Probe the endpoint (live) or audit captured headers (offline).
- Explain each finding and why it is exploitable (e.g. reflected origin + credentials = cross-origin data theft).
- Recommend an explicit origin allowlist and dropping credentials where not needed.
- Only test APIs the user owns or is authorized to test.
Files (claude-security-skills)
-
tests
-
test_auditor.py 5.3 KB
import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import auditor def _ids(headers, sent_origin=None): return {f.id for f in auditor.audit_cors(headers, sent_origin)} def test_no_cors_headers_clean(): assert auditor.audit_cors({"Content-Type": "text/html"}) == [] def test_wildcard_with_credentials_critical(): headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Credentials": "true", } findings = auditor.audit_cors(headers) assert any(f.id == "cors-wildcard-credentials" and f.severity == "critical" for f in findings) def test_wildcard_without_credentials_medium(): findings = auditor.audit_cors({"Access-Control-Allow-Origin": "*"}) assert any(f.id == "cors-wildcard" and f.severity == "medium" for f in findings) def test_null_origin_flagged(): assert "cors-null-origin" in _ids({"Access-Control-Allow-Origin": "null"}) def test_null_origin_with_credentials_critical(): headers = { "Access-Control-Allow-Origin": "null", "Access-Control-Allow-Credentials": "true", } findings = auditor.audit_cors(headers) assert any(f.id == "cors-null-origin" and f.severity == "critical" for f in findings) def test_reflected_origin_detected(): origin = "https://evil.example" headers = {"Access-Control-Allow-Origin": origin} assert "cors-reflected-origin" in _ids(headers, sent_origin=origin) def test_reflected_origin_with_credentials_critical(): origin = "https://evil.example" headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", } findings = auditor.audit_cors(headers, sent_origin=origin) assert any(f.id == "cors-reflected-origin" and f.severity == "critical" for f in findings) def test_fixed_origin_no_reflection(): headers = {"Access-Control-Allow-Origin": "https://app.example.com"} assert "cors-reflected-origin" not in _ids( headers, sent_origin="https://evil.example") def test_credentialed_fixed_origin_info(): headers = { "Access-Control-Allow-Origin": "https://app.example.com", "Access-Control-Allow-Credentials": "true", } assert "cors-credentials-enabled" in _ids(headers) def test_methods_wildcard_flagged(): headers = { "Access-Control-Allow-Origin": "https://app.example.com", "Access-Control-Allow-Methods": "*", } assert "cors-methods-wildcard" in _ids(headers) def test_parse_raw_headers(): text = ( "HTTP/1.1 200 OK\n" "Access-Control-Allow-Origin: *\n" "Content-Type: application/json\n" ) headers = auditor.parse_raw_headers(text) assert headers["Access-Control-Allow-Origin"] == "*" def test_cli_exit_code_high(tmp_path): f = tmp_path / "resp.txt" f.write_text("Access-Control-Allow-Origin: *\n" "Access-Control-Allow-Credentials: true\n") assert auditor.main(["--headers-file", str(f)]) == 1 def test_cli_exit_code_clean(tmp_path): f = tmp_path / "resp.txt" f.write_text("Access-Control-Allow-Origin: https://app.example.com\n") assert auditor.main(["--headers-file", str(f)]) == 0 def test_cli_json(tmp_path, capsys): import json f = tmp_path / "resp.txt" f.write_text("Access-Control-Allow-Origin: null\n") auditor.main(["--headers-file", str(f), "--json"]) data = json.loads(capsys.readouterr().out) assert isinstance(data, list) and data[0]["id"] == "cors-null-origin" def test_cli_medium_finding_exits_one(tmp_path): """A reported finding fails the build regardless of severity (issue #46).""" f = tmp_path / "resp.txt" f.write_text("Access-Control-Allow-Origin: *\n") assert auditor.main(["--headers-file", str(f)]) == 1 def test_cli_min_severity_filters_report_and_exit(tmp_path, capsys): import json f = tmp_path / "resp.txt" f.write_text("Access-Control-Allow-Origin: *\n") rc = auditor.main(["--headers-file", str(f), "--min-severity", "high", "--json"]) assert json.loads(capsys.readouterr().out) == [] assert rc == 0 def test_filter_by_severity(): findings = auditor.audit_cors({"Access-Control-Allow-Origin": "*"}) assert auditor.filter_by_severity(findings, "medium") == findings assert auditor.filter_by_severity(findings, "high") == [] INFO_ONLY_HEADERS = ("Access-Control-Allow-Origin: https://app.example.com\n" "Access-Control-Allow-Credentials: true\n") def test_cli_info_finding_reported_by_default(tmp_path, capsys): """INFO findings stay in the default report and still fail the build.""" import json f = tmp_path / "resp.txt" f.write_text(INFO_ONLY_HEADERS) rc = auditor.main(["--headers-file", str(f), "--json"]) data = json.loads(capsys.readouterr().out) assert [d["id"] for d in data] == ["cors-credentials-enabled"] assert [d["severity"] for d in data] == ["info"] assert rc == 1 def test_cli_min_severity_low_hides_info(tmp_path, capsys): import json f = tmp_path / "resp.txt" f.write_text(INFO_ONLY_HEADERS) rc = auditor.main(["--headers-file", str(f), "--min-severity", "low", "--json"]) assert json.loads(capsys.readouterr().out) == [] assert rc == 0
-
-
auditor.py 6.1 KB
from __future__ import annotations import argparse import json import sys from dataclasses import dataclass, asdict SEV_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4} PROBE_ORIGIN = "https://cors-probe.invalid" @dataclass class Finding: id: str severity: str message: str recommendation: str def to_dict(self) -> dict: return asdict(self) def _lower(headers: dict) -> dict: return {str(k).lower(): str(v) for k, v in headers.items()} def audit_cors(headers: dict, sent_origin: str | None = None) -> list: h = _lower(headers) out: list = [] if "access-control-allow-origin" not in h: return out acao = h["access-control-allow-origin"].strip() acac = h.get("access-control-allow-credentials", "").strip().lower() == "true" acam = h.get("access-control-allow-methods", "") if acao == "*": if acac: out.append(Finding( "cors-wildcard-credentials", "critical", "Access-Control-Allow-Origin is '*' together with " "Allow-Credentials: true.", "Never combine a wildcard origin with credentials; echo a " "vetted origin from an allowlist instead.")) else: out.append(Finding( "cors-wildcard", "medium", "Access-Control-Allow-Origin is '*' — any site can read " "responses.", "Restrict to an explicit allowlist of trusted origins.")) elif acao.lower() == "null": out.append(Finding( "cors-null-origin", "critical" if acac else "high", "Access-Control-Allow-Origin allows 'null', reachable from sandboxed " "iframes and local files.", "Never allow the 'null' origin; use explicit https origins.")) elif sent_origin and acao == sent_origin: out.append(Finding( "cors-reflected-origin", "critical" if acac else "high", f"The server reflects an arbitrary Origin ({sent_origin}) back in " "Access-Control-Allow-Origin.", "Validate Origin against an allowlist; do not echo it blindly.")) if acac and acao == "*": pass elif acac and acao not in ("", "*") and acao.lower() != "null" \ and not (sent_origin and acao == sent_origin): out.append(Finding( "cors-credentials-enabled", "info", f"Credentialed CORS is enabled for origin '{acao}'.", "Confirm this origin is fully trusted; credentials expose " "authenticated data.")) if "*" in [m.strip() for m in acam.split(",")]: out.append(Finding( "cors-methods-wildcard", "low", "Access-Control-Allow-Methods is '*'.", "List only the methods the endpoint actually needs.")) out.sort(key=lambda f: SEV_RANK.get(f.severity, 9)) return out def fetch_cors(url: str, origin: str = PROBE_ORIGIN, timeout: float = 10.0): import urllib.request req = urllib.request.Request( url, method="GET", headers={"Origin": origin, "User-Agent": "cors-auditor/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: return dict(resp.headers.items()) def parse_raw_headers(text: str) -> dict: headers: dict = {} for line in text.splitlines(): if not line.strip() or line.startswith("HTTP/") or ":" not in line: continue key, val = line.split(":", 1) headers[key.strip()] = val.strip() return headers def filter_by_severity(findings: list, min_severity: str = "info") -> list: threshold = SEV_RANK.get(min_severity, SEV_RANK["info"]) return [f for f in findings if SEV_RANK.get(f.severity, 3) <= threshold] def render(target: str, findings: list) -> str: if not findings: return f"[cors-auditor] {target}: no CORS misconfigurations found. [OK]" out = [f"[cors-auditor] {target}: {len(findings)} finding(s):\n"] for f in findings: out.append(f" [{f.severity.upper():<8}] {f.id}") out.append(f" {f.message}") out.append(f" fix: {f.recommendation}") return "\n".join(out) def main(argv: list | None = None) -> int: p = argparse.ArgumentParser( prog="cors-auditor", description="Audit a site's CORS configuration for misconfigurations.") p.add_argument("url", nargs="?", help="URL to probe (https://...)") p.add_argument("--origin", default=PROBE_ORIGIN, help="Origin header to send when probing reflection") p.add_argument("--headers-file", help="offline: file with a raw HTTP response header block") p.add_argument("--json", action="store_true", help="emit JSON") p.add_argument("--min-severity", default="info", choices=list(SEV_RANK), help="report findings at or above this severity " "(default: info, i.e. report everything)") args = p.parse_args(argv) if args.headers_file: try: with open(args.headers_file, "r", encoding="utf-8", errors="ignore") as fh: headers = parse_raw_headers(fh.read()) except OSError as exc: print(f"error: {exc}", file=sys.stderr) return 2 target = args.headers_file sent_origin = args.origin if args.origin != PROBE_ORIGIN else None elif args.url: try: headers = fetch_cors(args.url, args.origin) except Exception as exc: print(f"error: could not fetch {args.url}: {exc}", file=sys.stderr) return 2 target = args.url sent_origin = args.origin else: p.error("provide a URL or --headers-file") return 2 findings = filter_by_severity( audit_cors(headers, sent_origin=sent_origin), args.min_severity) if args.json: print(json.dumps([f.to_dict() for f in findings], indent=2)) else: print(render(target, findings)) return 1 if findings else 0 if __name__ == "__main__": try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass raise SystemExit(main()) -
SKILL.md 2.7 KB
--- name: cors-auditor description: >- Audit a site's Cross-Origin Resource Sharing (CORS) configuration for misconfigurations — wildcard origin with credentials, reflected arbitrary Origin, the 'null' origin, overly broad allowed methods, and risky credentialed CORS. Use when the user asks to "check my CORS config", "is my API's CORS safe", "test for CORS misconfiguration", or "why can any site call my API". license: MIT --- # CORS Misconfiguration Auditor Inspects CORS response headers and reports exploitable misconfigurations with fixes. The analysis core is pure and offline-testable; live probing uses only stdlib `urllib` and sends a throwaway `Origin` header to detect origin reflection. ## When to use this skill - "Audit the CORS configuration of https://api.example.com." - "Is it safe that my API returns Access-Control-Allow-Origin: *?" - "Does my server reflect any Origin back?" ## What it checks - **Wildcard + credentials** — `Allow-Origin: *` with `Allow-Credentials: true` (`cors-wildcard-credentials`). - **Reflected origin** — server echoes the request Origin back (`cors-reflected-origin`); critical when combined with credentials. - **Null origin** — `Allow-Origin: null` (`cors-null-origin`). - **Wildcard origin** — `Allow-Origin: *` without credentials (`cors-wildcard`). - **Credentialed CORS** — informational note when credentials are enabled (`cors-credentials-enabled`). - **Wildcard methods** — `Allow-Methods: *` (`cors-methods-wildcard`). ## How to run it ```bash # Live probe (sends a throwaway Origin to test reflection) python skills/cors-auditor/auditor.py https://api.example.com # Probe with a specific origin python skills/cors-auditor/auditor.py https://api.example.com --origin https://evil.example # Offline: audit a captured header block; pass --origin to test reflection python skills/cors-auditor/auditor.py --headers-file resp.txt --origin https://evil.example # Only fail CI on high/critical (hides the medium wildcard + info notes) python skills/cors-auditor/auditor.py https://api.example.com --min-severity high ``` **Exit codes:** `0` clean · `1` findings reported · `2` fetch/usage error. Every reported finding fails the build. The default reports everything down to `info` (including `cors-credentials-enabled`); raise `--min-severity` to `low`/`medium`/`high` to filter advisory notes out of both the report and the exit code. ## Recommended workflow for Claude 1. Probe the endpoint (live) or audit captured headers (offline). 2. Explain each finding and why it is exploitable (e.g. reflected origin + credentials = cross-origin data theft). 3. Recommend an explicit origin allowlist and dropping credentials where not needed. 4. Only test APIs the user owns or is authorized to test.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.