Security Headers Auditor Your site passes all the security scanners until someone iframes it, injects a script through an open CDN source in your CSP, or steals credentials from a page with no HSTS preload. This skill fetches your response headers, grades each security header against OWASP and Mozilla Observatory standards, and gives you the exact config line to add to nginx, Apache, Next.js, or Cloudflare Workers. Works against any URL via curl. Zero external API. --- Trigger Phrases - "security headers", "check my headers", "http headers audit" - "CSP audit", "content security policy" - "HS…

, directive_val):\n grade = 'C' if grade == 'A' else grade\n issues.append(f\"Wildcard source (*) in {directive_name} — allows loading from any domain\")\n fixes.append(f\"Replace * in {directive_name} with explicit trusted domains\")\n\n # Missing object-src\n if 'object-src' not in directives and 'default-src' not in directives:\n grade = 'B' if grade == 'A' else grade\n issues.append(\"Missing object-src — allows Flash/plugin injection if default-src not set\")\n fixes.append(\"Add: object-src 'none'\")\n\n # Missing base-uri\n if 'base-uri' not in directives:\n issues.append(\"Missing base-uri — allows \u003cbase> tag injection to hijack relative URLs\")\n fixes.append(\"Add: base-uri 'self'\")\n\n # report-uri / report-to\n if 'report-uri' not in directives and 'report-to' not in directives:\n issues.append(\"No CSP violation reporting configured — violations are silent\")\n fixes.append(\"Add: report-to /csp-violations (or use report-uri https://yoursite.com/csp-report)\")\n\n return HeaderCheck(\n name='Content-Security-Policy', present=True, value=value,\n grade=grade, issues=issues, fixes=fixes, severity='CRITICAL' if grade == 'F' else 'HIGH'\n )\n\n\n# ── HSTS ─────────────────────────────────────────────────────────────────────\n\ndef check_hsts(value: Optional[str], is_https: bool = True) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='Strict-Transport-Security', present=False, value=None, grade='F',\n issues=['HSTS missing — browser allows HTTP downgrade attacks'],\n fixes=[\n \"nginx: add_header Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\" always;\",\n \"Apache: Header always set Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\"\",\n ],\n severity='HIGH',\n )\n\n issues = []\n fixes = []\n grade = 'A'\n\n # max-age\n max_age_match = re.search(r'max-age=(\\d+)', value, re.I)\n if max_age_match:\n max_age = int(max_age_match.group(1))\n if max_age \u003c 2592000: # 30 days\n grade = 'C'\n issues.append(f\"max-age={max_age} is too short (\u003c 30 days) — not eligible for preload list\")\n fixes.append(\"Set max-age=31536000 (1 year) minimum for preload eligibility\")\n elif max_age \u003c 31536000:\n grade = 'B'\n issues.append(f\"max-age={max_age} — recommend 31536000 (1 year) for preload eligibility\")\n else:\n grade = 'F'\n issues.append(\"max-age directive missing from HSTS header\")\n fixes.append(\"Add max-age=31536000 to HSTS header\")\n\n # includeSubDomains\n if 'includesubdomains' not in value.lower():\n grade = 'B' if grade == 'A' else grade\n issues.append(\"Missing includeSubDomains — subdomains can be downgraded to HTTP\")\n fixes.append(\"Add includeSubDomains directive\")\n\n # preload\n if 'preload' not in value.lower():\n issues.append(\"Missing preload directive — site not eligible for HSTS preload list\")\n fixes.append(\"Add preload directive and submit to hstspreload.org\")\n\n return HeaderCheck(\n name='Strict-Transport-Security', present=True, value=value,\n grade=grade, issues=issues, fixes=fixes, severity='HIGH' if grade != 'A' else 'INFO'\n )\n\n\n# ── X-Frame-Options ──────────────────────────────────────────────────────────\n\ndef check_xfo(value: Optional[str], csp_value: Optional[str] = None) -> HeaderCheck:\n # If CSP has frame-ancestors, XFO is redundant (CSP takes precedence)\n if csp_value and 'frame-ancestors' in csp_value.lower():\n return HeaderCheck(\n name='X-Frame-Options', present=bool(value), value=value, grade='A',\n issues=[], fixes=[\"frame-ancestors in CSP supersedes X-Frame-Options (modern browsers)\"],\n severity='INFO'\n )\n\n if not value:\n return HeaderCheck(\n name='X-Frame-Options', present=False, value=None, grade='F',\n issues=['Missing — site is vulnerable to clickjacking attacks'],\n fixes=[\n \"nginx: add_header X-Frame-Options \\\"DENY\\\" always;\",\n \"Or add to CSP: frame-ancestors 'none' (preferred — supports more granular rules)\",\n ],\n severity='HIGH',\n )\n\n val_upper = value.strip().upper()\n if val_upper == 'DENY':\n return HeaderCheck(name='X-Frame-Options', present=True, value=value, grade='A',\n issues=[], fixes=[], severity='INFO')\n elif val_upper == 'SAMEORIGIN':\n return HeaderCheck(name='X-Frame-Options', present=True, value=value, grade='B',\n issues=[\"SAMEORIGIN allows framing from same origin — DENY is stronger\"],\n fixes=[\"Change to DENY unless you deliberately embed the page in an iframe\"],\n severity='LOW')\n else:\n return HeaderCheck(name='X-Frame-Options', present=True, value=value, grade='C',\n issues=[f\"Non-standard value: {value}\"],\n fixes=[\"Use DENY or SAMEORIGIN\"], severity='MEDIUM')\n\n\n# ── X-Content-Type-Options ────────────────────────────────────────────────────\n\ndef check_xcto(value: Optional[str]) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='X-Content-Type-Options', present=False, value=None, grade='F',\n issues=['Missing — browser may MIME-sniff responses enabling content injection'],\n fixes=[\"nginx: add_header X-Content-Type-Options \\\"nosniff\\\" always;\"],\n severity='MEDIUM',\n )\n if value.strip().lower() == 'nosniff':\n return HeaderCheck(name='X-Content-Type-Options', present=True, value=value,\n grade='A', issues=[], fixes=[], severity='INFO')\n return HeaderCheck(name='X-Content-Type-Options', present=True, value=value,\n grade='C', issues=[f\"Unexpected value: {value} (expected nosniff)\"],\n fixes=[\"Set to nosniff\"], severity='LOW')\n\n\n# ── Referrer-Policy ──────────────────────────────────────────────────────────\n\nSAFE_REFERRER_POLICIES = {\n 'no-referrer', 'no-referrer-when-downgrade',\n 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin'\n}\n\ndef check_referrer_policy(value: Optional[str]) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='Referrer-Policy', present=False, value=None, grade='C',\n issues=['Missing — browser defaults to no-referrer-when-downgrade (leaks full URL to same-HTTPS origins)'],\n fixes=[\"nginx: add_header Referrer-Policy \\\"strict-origin-when-cross-origin\\\" always;\"],\n severity='LOW',\n )\n if value.strip().lower() in SAFE_REFERRER_POLICIES:\n return HeaderCheck(name='Referrer-Policy', present=True, value=value,\n grade='A', issues=[], fixes=[], severity='INFO')\n if value.strip().lower() in ('unsafe-url', 'origin-when-cross-origin'):\n return HeaderCheck(name='Referrer-Policy', present=True, value=value, grade='F',\n issues=[f\"'{value}' leaks full URL or origin to cross-origin requests\"],\n fixes=[\"Change to strict-origin-when-cross-origin\"],\n severity='MEDIUM')\n return HeaderCheck(name='Referrer-Policy', present=True, value=value, grade='B',\n issues=[], fixes=[], severity='INFO')\n\n\n# ── Permissions-Policy ───────────────────────────────────────────────────────\n\nSENSITIVE_FEATURES = ['camera', 'microphone', 'geolocation', 'payment', 'usb',\n 'fullscreen', 'accelerometer', 'gyroscope', 'magnetometer',\n 'ambient-light-sensor', 'battery', 'screen-wake-lock']\n\ndef check_permissions_policy(value: Optional[str]) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='Permissions-Policy', present=False, value=None, grade='C',\n issues=['Missing — browser APIs (camera, mic, geolocation) may be accessible to injected scripts'],\n fixes=[\n \"nginx: add_header Permissions-Policy \"\n \"\\\"camera=(), microphone=(), geolocation=(), payment=(), usb=()\\\" always;\",\n ],\n severity='LOW',\n )\n issues = []\n directives = {d.split('=')[0].strip().lower(): d for d in value.split(',') if d.strip()}\n unconstrained = [f for f in SENSITIVE_FEATURES if f not in directives]\n if unconstrained:\n issues.append(f\"Sensitive features not explicitly disabled: {', '.join(unconstrained)}\")\n return HeaderCheck(\n name='Permissions-Policy', present=True, value=value,\n grade='A' if not unconstrained else 'B',\n issues=issues,\n fixes=[f\"Add {f}=() to Permissions-Policy to disable it\" for f in unconstrained[:3]],\n severity='LOW' if issues else 'INFO'\n )\n\n\n# ── Cross-Origin Isolation (CORP / COEP / COOP) ──────────────────────────────\n\ndef check_cross_origin_isolation(headers: dict) -> list[HeaderCheck]:\n checks = []\n\n coep = headers.get('cross-origin-embedder-policy')\n coop = headers.get('cross-origin-opener-policy')\n corp = headers.get('cross-origin-resource-policy')\n\n if not coep:\n checks.append(HeaderCheck(\n name='Cross-Origin-Embedder-Policy', present=False, value=None, grade='C',\n issues=['Missing COEP — cross-origin isolation not achievable (needed for SharedArrayBuffer)'],\n fixes=[\"add_header Cross-Origin-Embedder-Policy \\\"require-corp\\\" always;\"],\n severity='LOW'\n ))\n else:\n checks.append(HeaderCheck(\n name='Cross-Origin-Embedder-Policy', present=True, value=coep,\n grade='A', issues=[], fixes=[], severity='INFO'\n ))\n\n if not coop:\n checks.append(HeaderCheck(\n name='Cross-Origin-Opener-Policy', present=False, value=None, grade='C',\n issues=['Missing COOP — page may be accessible from cross-origin windows (Spectre risk)'],\n fixes=[\"add_header Cross-Origin-Opener-Policy \\\"same-origin\\\" always;\"],\n severity='LOW'\n ))\n else:\n checks.append(HeaderCheck(\n name='Cross-Origin-Opener-Policy', present=True, value=coop,\n grade='A', issues=[], fixes=[], severity='INFO'\n ))\n\n return checks\n\n\ndef audit_security_headers(url: str) -> list[HeaderCheck]:\n \"\"\"Run full security header audit against a URL.\"\"\"\n headers = fetch_headers(url)\n is_https = url.startswith('https')\n\n csp_val = headers.get('content-security-policy')\n results = [\n check_csp(csp_val),\n check_hsts(headers.get('strict-transport-security'), is_https),\n check_xfo(headers.get('x-frame-options'), csp_val),\n check_xcto(headers.get('x-content-type-options')),\n check_referrer_policy(headers.get('referrer-policy')),\n check_permissions_policy(headers.get('permissions-policy')),\n ]\n results.extend(check_cross_origin_isolation(headers))\n return results\n```\n\n---\n\n## Step 3: Calculate Grade\n\n```python\nGRADE_WEIGHTS = {\n 'Content-Security-Policy': 3, # Most impactful\n 'Strict-Transport-Security': 2,\n 'X-Frame-Options': 2,\n 'X-Content-Type-Options': 1,\n 'Referrer-Policy': 1,\n 'Permissions-Policy': 1,\n 'Cross-Origin-Embedder-Policy': 0.5,\n 'Cross-Origin-Opener-Policy': 0.5,\n}\n\nGRADE_SCORE = {'A': 100, 'B': 75, 'C': 50, 'F': 0, 'MISSING': 0}\n\n\ndef overall_grade(checks: list) -> tuple[str, int]:\n \"\"\"Compute weighted average score and letter grade.\"\"\"\n total_weight = 0\n weighted_score = 0\n for check in checks:\n w = GRADE_WEIGHTS.get(check.name, 1)\n score = GRADE_SCORE.get(check.grade, 0)\n weighted_score += score * w\n total_weight += w\n\n pct = int(weighted_score / total_weight) if total_weight else 0\n\n if pct >= 90: letter = 'A'\n elif pct >= 75: letter = 'B'\n elif pct >= 50: letter = 'C'\n elif pct >= 25: letter = 'D'\n else: letter = 'F'\n\n return letter, pct\n```\n\n---\n\n## Step 4: Generate Config Fixes\n\n```python\ndef generate_nginx_config(checks: list) -> str:\n \"\"\"Generate a complete nginx add_header block for all failing headers.\"\"\"\n lines = [\"# Security Headers — generated by phy-security-headers\", \"\"]\n\n NGINX_TEMPLATES = {\n 'Content-Security-Policy': (\n \"add_header Content-Security-Policy \"\n \"\\\"default-src 'self'; script-src 'self' 'nonce-REPLACE_WITH_NONCE'; \"\n \"object-src 'none'; base-uri 'self'; frame-ancestors 'none'; \"\n \"upgrade-insecure-requests;\\\" always;\"\n ),\n 'Strict-Transport-Security':\n \"add_header Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\" always;\",\n 'X-Frame-Options':\n \"add_header X-Frame-Options \\\"DENY\\\" always;\",\n 'X-Content-Type-Options':\n \"add_header X-Content-Type-Options \\\"nosniff\\\" always;\",\n 'Referrer-Policy':\n \"add_header Referrer-Policy \\\"strict-origin-when-cross-origin\\\" always;\",\n 'Permissions-Policy':\n \"add_header Permissions-Policy \\\"camera=(), microphone=(), geolocation=(), payment=(), usb=()\\\" always;\",\n 'Cross-Origin-Embedder-Policy':\n \"add_header Cross-Origin-Embedder-Policy \\\"require-corp\\\" always;\",\n 'Cross-Origin-Opener-Policy':\n \"add_header Cross-Origin-Opener-Policy \\\"same-origin\\\" always;\",\n 'Cross-Origin-Resource-Policy':\n \"add_header Cross-Origin-Resource-Policy \\\"same-origin\\\" always;\",\n }\n\n for check in checks:\n if check.grade in ('F', 'C', 'MISSING') and check.name in NGINX_TEMPLATES:\n lines.append(NGINX_TEMPLATES[check.name])\n\n return '\\n'.join(lines)\n\n\ndef generate_nextjs_config(checks: list) -> str:\n \"\"\"Generate Next.js next.config.js headers() array.\"\"\"\n lines = [\n \"// next.config.js — Security Headers\",\n \"const securityHeaders = [\",\n ]\n NEXTJS_TEMPLATES = {\n 'Content-Security-Policy': (\n \" { key: 'Content-Security-Policy', \"\n \"value: \\\"default-src 'self'; script-src 'self'; object-src 'none'; \"\n \"base-uri 'self'; frame-ancestors 'none';\\\" },\"\n ),\n 'Strict-Transport-Security':\n \" { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },\",\n 'X-Frame-Options':\n \" { key: 'X-Frame-Options', value: 'DENY' },\",\n 'X-Content-Type-Options':\n \" { key: 'X-Content-Type-Options', value: 'nosniff' },\",\n 'Referrer-Policy':\n \" { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\",\n 'Permissions-Policy':\n \" { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },\",\n }\n for check in checks:\n if check.grade in ('F', 'C', 'MISSING') and check.name in NEXTJS_TEMPLATES:\n lines.append(NEXTJS_TEMPLATES[check.name])\n lines.extend([\n \"];\",\n \"\",\n \"module.exports = {\",\n \" async headers() {\",\n \" return [{ source: '/(.*)', headers: securityHeaders }];\",\n \" },\",\n \"};\",\n ])\n return '\\n'.join(lines)\n```\n\n---\n\n## Step 5: Output Report\n\n```markdown\n## Security Headers Audit\nURL: https://myapp.com | Checked: 2026-03-19 09:22 UTC\n\n---\n\n### Overall Grade: C (54/100)\n\n| Header | Grade | Status | Key Issue |\n|--------|-------|--------|-----------|\n| Content-Security-Policy | F | 🔴 FAIL | 'unsafe-inline' in script-src |\n| Strict-Transport-Security | B | 🟡 REVIEW | max-age=86400 too short, no preload |\n| X-Frame-Options | A | ✅ PASS | DENY |\n| X-Content-Type-Options | A | ✅ PASS | nosniff |\n| Referrer-Policy | C | 🟠 WARN | Missing |\n| Permissions-Policy | C | 🟠 WARN | Missing |\n| Cross-Origin-Embedder-Policy | C | 🟠 WARN | Missing |\n| Cross-Origin-Opener-Policy | C | 🟠 WARN | Missing |\n\n---\n\n### 🔴 CRITICAL — Content-Security-Policy\n\n**Found:** `default-src 'self' 'unsafe-inline' https://cdn.example.com`\n\n**Issue 1:** `'unsafe-inline'` in default-src — completely negates XSS protection.\nAny injected `\u003cscript>` will execute. This is as bad as no CSP.\n\n**Fix:**\n```html\n\u003c!-- Replace unsafe-inline with nonce-based CSP -->\n\u003c!-- In your HTML template, add nonce to every script tag: -->\n\u003cscript nonce=\"{{CSP_NONCE}}\">...\u003c/script>\n\n\u003c!-- Set header with matching nonce: -->\nContent-Security-Policy: default-src 'self';\n script-src 'self' 'nonce-ABC123' 'strict-dynamic';\n object-src 'none';\n base-uri 'self';\n frame-ancestors 'none';\n```\n\n```python\n# Express.js nonce middleware:\nimport secrets\napp.use((req, res, next) => {\n res.locals.nonce = secrets.token_hex(16)\n res.setHeader('Content-Security-Policy',\n `script-src 'nonce-${res.locals.nonce}' 'strict-dynamic'`)\n next()\n})\n```\n\n---\n\n### 🟡 REVIEW — Strict-Transport-Security\n\n**Found:** `max-age=86400`\n\n**Issue:** max-age=86400 (1 day) — too short for HSTS preload eligibility (minimum 1 year).\nBrowser will stop enforcing HTTPS after 24 hours if a user hasn't visited recently.\n\n**Fix:**\n```nginx\nadd_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n```\n\n**Submit to preload list:** After fixing → https://hstspreload.org\n\n---\n\n### 🟠 MISSING — Referrer-Policy\n\n**Fix:**\n```nginx\nadd_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n```\n\n---\n\n### Generated Config Fixes\n\n**nginx (add to server block):**\n```nginx\n# Security Headers — generated by phy-security-headers\nadd_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'nonce-REPLACE_WITH_NONCE'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests;\" always;\nadd_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\nadd_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\nadd_header Permissions-Policy \"camera=(), microphone=(), geolocation=(), payment=(), usb=()\" always;\nadd_header Cross-Origin-Embedder-Policy \"require-corp\" always;\nadd_header Cross-Origin-Opener-Policy \"same-origin\" always;\n```\n\n**Verify after deploy:**\n```bash\ncurl -sI https://myapp.com | grep -i -E \"content-security|strict-transport|x-frame|x-content|referrer|permissions\"\n```\n\n---\n\n### Mozilla Observatory Equivalent Score\n\nThis audit maps to Mozilla Observatory (observatory.mozilla.org) scoring:\n- CSP with unsafe-inline: -25 pts\n- HSTS max-age \u003c 6 months: -5 pts\n- Missing Referrer-Policy: -5 pts\n- Missing Permissions-Policy: -5 pts\n\n**Estimated Observatory Score: ~45/100 (D)**\nFix the CSP first — it alone accounts for 25 points.\n```\n\n---\n\n## Quick Mode Output\n\n```\nSecurity Headers: https://myapp.com\n\nOverall: C (54/100)\n\n🔴 CSP: F — 'unsafe-inline' negates XSS protection (fix: nonce-based CSP)\n🟡 HSTS: B — max-age=86400 too short (fix: 31536000 + preload)\n✅ X-Frame-Options: A — DENY\n✅ X-Content-Type-Options: A — nosniff\n🟠 Referrer-Policy: MISSING\n🟠 Permissions-Policy: MISSING\n🟠 COEP/COOP: MISSING\n\nPriority fix: Replace 'unsafe-inline' with nonce → lifts score to A (89/100)\n```\n---","attachment_filenames":["_meta.json"],"attachments":[{"filename":"_meta.json","content":"{\n \"owner\": \"phy041\",\n \"slug\": \"phy-security-headers\",\n \"displayName\": \"Phy Security Headers\",\n \"latest\": {\n \"version\": \"1.0.0\",\n \"publishedAt\": 1774044864824,\n \"commit\": \"https://github.com/openclaw/skills/commit/baa7cb720362dd7dd81b25d0001b4890e2ffa688\"\n },\n \"history\": []\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":292,"content_sha256":"48a55a6eaf01e46780d34eda31d9dce9f8fee03029bfdd5c5bc84944d38cbe2d"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Security Headers Auditor","type":"text"}]},{"type":"paragraph","content":[{"text":"Your site passes all the security scanners until someone iframes it, injects a script through an open CDN source in your CSP, or steals credentials from a page with no HSTS preload.","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill fetches your response headers, grades each security header against OWASP and Mozilla Observatory standards, and gives you the exact config line to add to nginx, Apache, Next.js, or Cloudflare Workers.","type":"text"}]},{"type":"paragraph","content":[{"text":"Works against any URL via curl. Zero external API.","type":"text","marks":[{"type":"strong"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Trigger Phrases","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"security headers\", \"check my headers\", \"http headers audit\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"CSP audit\", \"content security policy\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"HSTS missing\", \"HSTS preload\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"clickjacking protection\", \"X-Frame-Options\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"mozilla observatory\", \"OWASP headers\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Permissions-Policy\", \"CORP COEP COOP\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"/sec-headers\"","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Provide Input","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Option 1: Audit a live URL\n/sec-headers https://myapp.com\n\n# Option 2: Audit local dev server\n/sec-headers http://localhost:3000\n\n# Option 3: Audit specific path\n/sec-headers https://myapp.com/dashboard\n\n# Option 4: Audit with custom port\n/sec-headers https://staging.myapp.com:8443\n\n# Option 5: Grade only (no fix suggestions)\n/sec-headers https://myapp.com --grade-only\n\n# Option 6: Generate nginx config snippet\n/sec-headers https://myapp.com --output nginx\n\n# Option 7: Generate Next.js headers config\n/sec-headers https://myapp.com --output nextjs","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 1: Fetch Headers","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Fetch all response headers\nURL=\"https://myapp.com\"\n\ncurl -sI \"$URL\" \\\n --max-time 10 \\\n --user-agent \"SecurityHeaderAuditor/1.0\" \\\n -L # follow redirects\n\n# Also check www redirect behavior\ncurl -sI \"http://$URL\" 2>/dev/null | head -5\n\n# Save raw headers for parsing\nHEADERS=$(curl -sI \"$URL\" --max-time 10 -L 2>/dev/null)\necho \"$HEADERS\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 2: Grade Each Header","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import re\nimport subprocess\nimport sys\nfrom dataclasses import dataclass, field\nfrom typing import Optional\n\n\n@dataclass\nclass HeaderCheck:\n name: str\n present: bool\n value: Optional[str]\n grade: str # A / B / C / F / MISSING\n issues: list[str]\n fixes: list[str]\n severity: str # CRITICAL / HIGH / MEDIUM / LOW / INFO\n\n\ndef fetch_headers(url: str) -> dict[str, str]:\n \"\"\"Fetch HTTP response headers from URL.\"\"\"\n result = subprocess.run(\n ['curl', '-sI', url, '--max-time', '10', '-L',\n '--user-agent', 'SecurityHeaderAuditor/1.0'],\n capture_output=True, text=True\n )\n headers = {}\n for line in result.stdout.splitlines():\n if ':' in line and not line.startswith('HTTP/'):\n key, _, value = line.partition(':')\n headers[key.strip().lower()] = value.strip()\n return headers\n\n\n# ── Content-Security-Policy ──────────────────────────────────────────────────\n\ndef check_csp(value: Optional[str]) -> HeaderCheck:\n issues = []\n fixes = []\n grade = 'A'\n\n if not value:\n return HeaderCheck(\n name='Content-Security-Policy',\n present=False, value=None, grade='F',\n issues=['CSP header is missing — XSS attacks have no browser-level mitigation'],\n fixes=[\n \"Add to nginx: add_header Content-Security-Policy \"\n \"\\\"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';\\\" always;\",\n \"Next.js (next.config.js headers): { key: 'Content-Security-Policy', value: \\\"default-src 'self'...\\\" }\",\n ],\n severity='CRITICAL',\n )\n\n directives = {d.split()[0].lower(): d for d in value.split(';') if d.strip()}\n\n # unsafe-inline in script-src — CRITICAL\n script_src = directives.get('script-src', directives.get('default-src', ''))\n if \"'unsafe-inline'\" in script_src:\n grade = 'F'\n issues.append(\"'unsafe-inline' in script-src — negates XSS protection entirely\")\n fixes.append(\"Replace 'unsafe-inline' with a nonce: script-src 'nonce-{random}' 'strict-dynamic'\")\n\n # unsafe-eval\n if \"'unsafe-eval'\" in script_src:\n grade = 'C' if grade == 'A' else grade\n issues.append(\"'unsafe-eval' allows eval() — enables second-order XSS\")\n fixes.append(\"Remove 'unsafe-eval'; refactor code using eval() to use Function() alternatives\")\n\n # Wildcard source\n for directive_name, directive_val in directives.items():\n if re.search(r'\\bhttps?://\\*\\b|^\\*

Security Headers Auditor Your site passes all the security scanners until someone iframes it, injects a script through an open CDN source in your CSP, or steals credentials from a page with no HSTS preload. This skill fetches your response headers, grades each security header against OWASP and Mozilla Observatory standards, and gives you the exact config line to add to nginx, Apache, Next.js, or Cloudflare Workers. Works against any URL via curl. Zero external API. --- Trigger Phrases - "security headers", "check my headers", "http headers audit" - "CSP audit", "content security policy" - "HS…

, directive_val):\n grade = 'C' if grade == 'A' else grade\n issues.append(f\"Wildcard source (*) in {directive_name} — allows loading from any domain\")\n fixes.append(f\"Replace * in {directive_name} with explicit trusted domains\")\n\n # Missing object-src\n if 'object-src' not in directives and 'default-src' not in directives:\n grade = 'B' if grade == 'A' else grade\n issues.append(\"Missing object-src — allows Flash/plugin injection if default-src not set\")\n fixes.append(\"Add: object-src 'none'\")\n\n # Missing base-uri\n if 'base-uri' not in directives:\n issues.append(\"Missing base-uri — allows \u003cbase> tag injection to hijack relative URLs\")\n fixes.append(\"Add: base-uri 'self'\")\n\n # report-uri / report-to\n if 'report-uri' not in directives and 'report-to' not in directives:\n issues.append(\"No CSP violation reporting configured — violations are silent\")\n fixes.append(\"Add: report-to /csp-violations (or use report-uri https://yoursite.com/csp-report)\")\n\n return HeaderCheck(\n name='Content-Security-Policy', present=True, value=value,\n grade=grade, issues=issues, fixes=fixes, severity='CRITICAL' if grade == 'F' else 'HIGH'\n )\n\n\n# ── HSTS ─────────────────────────────────────────────────────────────────────\n\ndef check_hsts(value: Optional[str], is_https: bool = True) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='Strict-Transport-Security', present=False, value=None, grade='F',\n issues=['HSTS missing — browser allows HTTP downgrade attacks'],\n fixes=[\n \"nginx: add_header Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\" always;\",\n \"Apache: Header always set Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\"\",\n ],\n severity='HIGH',\n )\n\n issues = []\n fixes = []\n grade = 'A'\n\n # max-age\n max_age_match = re.search(r'max-age=(\\d+)', value, re.I)\n if max_age_match:\n max_age = int(max_age_match.group(1))\n if max_age \u003c 2592000: # 30 days\n grade = 'C'\n issues.append(f\"max-age={max_age} is too short (\u003c 30 days) — not eligible for preload list\")\n fixes.append(\"Set max-age=31536000 (1 year) minimum for preload eligibility\")\n elif max_age \u003c 31536000:\n grade = 'B'\n issues.append(f\"max-age={max_age} — recommend 31536000 (1 year) for preload eligibility\")\n else:\n grade = 'F'\n issues.append(\"max-age directive missing from HSTS header\")\n fixes.append(\"Add max-age=31536000 to HSTS header\")\n\n # includeSubDomains\n if 'includesubdomains' not in value.lower():\n grade = 'B' if grade == 'A' else grade\n issues.append(\"Missing includeSubDomains — subdomains can be downgraded to HTTP\")\n fixes.append(\"Add includeSubDomains directive\")\n\n # preload\n if 'preload' not in value.lower():\n issues.append(\"Missing preload directive — site not eligible for HSTS preload list\")\n fixes.append(\"Add preload directive and submit to hstspreload.org\")\n\n return HeaderCheck(\n name='Strict-Transport-Security', present=True, value=value,\n grade=grade, issues=issues, fixes=fixes, severity='HIGH' if grade != 'A' else 'INFO'\n )\n\n\n# ── X-Frame-Options ──────────────────────────────────────────────────────────\n\ndef check_xfo(value: Optional[str], csp_value: Optional[str] = None) -> HeaderCheck:\n # If CSP has frame-ancestors, XFO is redundant (CSP takes precedence)\n if csp_value and 'frame-ancestors' in csp_value.lower():\n return HeaderCheck(\n name='X-Frame-Options', present=bool(value), value=value, grade='A',\n issues=[], fixes=[\"frame-ancestors in CSP supersedes X-Frame-Options (modern browsers)\"],\n severity='INFO'\n )\n\n if not value:\n return HeaderCheck(\n name='X-Frame-Options', present=False, value=None, grade='F',\n issues=['Missing — site is vulnerable to clickjacking attacks'],\n fixes=[\n \"nginx: add_header X-Frame-Options \\\"DENY\\\" always;\",\n \"Or add to CSP: frame-ancestors 'none' (preferred — supports more granular rules)\",\n ],\n severity='HIGH',\n )\n\n val_upper = value.strip().upper()\n if val_upper == 'DENY':\n return HeaderCheck(name='X-Frame-Options', present=True, value=value, grade='A',\n issues=[], fixes=[], severity='INFO')\n elif val_upper == 'SAMEORIGIN':\n return HeaderCheck(name='X-Frame-Options', present=True, value=value, grade='B',\n issues=[\"SAMEORIGIN allows framing from same origin — DENY is stronger\"],\n fixes=[\"Change to DENY unless you deliberately embed the page in an iframe\"],\n severity='LOW')\n else:\n return HeaderCheck(name='X-Frame-Options', present=True, value=value, grade='C',\n issues=[f\"Non-standard value: {value}\"],\n fixes=[\"Use DENY or SAMEORIGIN\"], severity='MEDIUM')\n\n\n# ── X-Content-Type-Options ────────────────────────────────────────────────────\n\ndef check_xcto(value: Optional[str]) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='X-Content-Type-Options', present=False, value=None, grade='F',\n issues=['Missing — browser may MIME-sniff responses enabling content injection'],\n fixes=[\"nginx: add_header X-Content-Type-Options \\\"nosniff\\\" always;\"],\n severity='MEDIUM',\n )\n if value.strip().lower() == 'nosniff':\n return HeaderCheck(name='X-Content-Type-Options', present=True, value=value,\n grade='A', issues=[], fixes=[], severity='INFO')\n return HeaderCheck(name='X-Content-Type-Options', present=True, value=value,\n grade='C', issues=[f\"Unexpected value: {value} (expected nosniff)\"],\n fixes=[\"Set to nosniff\"], severity='LOW')\n\n\n# ── Referrer-Policy ──────────────────────────────────────────────────────────\n\nSAFE_REFERRER_POLICIES = {\n 'no-referrer', 'no-referrer-when-downgrade',\n 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin'\n}\n\ndef check_referrer_policy(value: Optional[str]) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='Referrer-Policy', present=False, value=None, grade='C',\n issues=['Missing — browser defaults to no-referrer-when-downgrade (leaks full URL to same-HTTPS origins)'],\n fixes=[\"nginx: add_header Referrer-Policy \\\"strict-origin-when-cross-origin\\\" always;\"],\n severity='LOW',\n )\n if value.strip().lower() in SAFE_REFERRER_POLICIES:\n return HeaderCheck(name='Referrer-Policy', present=True, value=value,\n grade='A', issues=[], fixes=[], severity='INFO')\n if value.strip().lower() in ('unsafe-url', 'origin-when-cross-origin'):\n return HeaderCheck(name='Referrer-Policy', present=True, value=value, grade='F',\n issues=[f\"'{value}' leaks full URL or origin to cross-origin requests\"],\n fixes=[\"Change to strict-origin-when-cross-origin\"],\n severity='MEDIUM')\n return HeaderCheck(name='Referrer-Policy', present=True, value=value, grade='B',\n issues=[], fixes=[], severity='INFO')\n\n\n# ── Permissions-Policy ───────────────────────────────────────────────────────\n\nSENSITIVE_FEATURES = ['camera', 'microphone', 'geolocation', 'payment', 'usb',\n 'fullscreen', 'accelerometer', 'gyroscope', 'magnetometer',\n 'ambient-light-sensor', 'battery', 'screen-wake-lock']\n\ndef check_permissions_policy(value: Optional[str]) -> HeaderCheck:\n if not value:\n return HeaderCheck(\n name='Permissions-Policy', present=False, value=None, grade='C',\n issues=['Missing — browser APIs (camera, mic, geolocation) may be accessible to injected scripts'],\n fixes=[\n \"nginx: add_header Permissions-Policy \"\n \"\\\"camera=(), microphone=(), geolocation=(), payment=(), usb=()\\\" always;\",\n ],\n severity='LOW',\n )\n issues = []\n directives = {d.split('=')[0].strip().lower(): d for d in value.split(',') if d.strip()}\n unconstrained = [f for f in SENSITIVE_FEATURES if f not in directives]\n if unconstrained:\n issues.append(f\"Sensitive features not explicitly disabled: {', '.join(unconstrained)}\")\n return HeaderCheck(\n name='Permissions-Policy', present=True, value=value,\n grade='A' if not unconstrained else 'B',\n issues=issues,\n fixes=[f\"Add {f}=() to Permissions-Policy to disable it\" for f in unconstrained[:3]],\n severity='LOW' if issues else 'INFO'\n )\n\n\n# ── Cross-Origin Isolation (CORP / COEP / COOP) ──────────────────────────────\n\ndef check_cross_origin_isolation(headers: dict) -> list[HeaderCheck]:\n checks = []\n\n coep = headers.get('cross-origin-embedder-policy')\n coop = headers.get('cross-origin-opener-policy')\n corp = headers.get('cross-origin-resource-policy')\n\n if not coep:\n checks.append(HeaderCheck(\n name='Cross-Origin-Embedder-Policy', present=False, value=None, grade='C',\n issues=['Missing COEP — cross-origin isolation not achievable (needed for SharedArrayBuffer)'],\n fixes=[\"add_header Cross-Origin-Embedder-Policy \\\"require-corp\\\" always;\"],\n severity='LOW'\n ))\n else:\n checks.append(HeaderCheck(\n name='Cross-Origin-Embedder-Policy', present=True, value=coep,\n grade='A', issues=[], fixes=[], severity='INFO'\n ))\n\n if not coop:\n checks.append(HeaderCheck(\n name='Cross-Origin-Opener-Policy', present=False, value=None, grade='C',\n issues=['Missing COOP — page may be accessible from cross-origin windows (Spectre risk)'],\n fixes=[\"add_header Cross-Origin-Opener-Policy \\\"same-origin\\\" always;\"],\n severity='LOW'\n ))\n else:\n checks.append(HeaderCheck(\n name='Cross-Origin-Opener-Policy', present=True, value=coop,\n grade='A', issues=[], fixes=[], severity='INFO'\n ))\n\n return checks\n\n\ndef audit_security_headers(url: str) -> list[HeaderCheck]:\n \"\"\"Run full security header audit against a URL.\"\"\"\n headers = fetch_headers(url)\n is_https = url.startswith('https')\n\n csp_val = headers.get('content-security-policy')\n results = [\n check_csp(csp_val),\n check_hsts(headers.get('strict-transport-security'), is_https),\n check_xfo(headers.get('x-frame-options'), csp_val),\n check_xcto(headers.get('x-content-type-options')),\n check_referrer_policy(headers.get('referrer-policy')),\n check_permissions_policy(headers.get('permissions-policy')),\n ]\n results.extend(check_cross_origin_isolation(headers))\n return results","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 3: Calculate Grade","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"GRADE_WEIGHTS = {\n 'Content-Security-Policy': 3, # Most impactful\n 'Strict-Transport-Security': 2,\n 'X-Frame-Options': 2,\n 'X-Content-Type-Options': 1,\n 'Referrer-Policy': 1,\n 'Permissions-Policy': 1,\n 'Cross-Origin-Embedder-Policy': 0.5,\n 'Cross-Origin-Opener-Policy': 0.5,\n}\n\nGRADE_SCORE = {'A': 100, 'B': 75, 'C': 50, 'F': 0, 'MISSING': 0}\n\n\ndef overall_grade(checks: list) -> tuple[str, int]:\n \"\"\"Compute weighted average score and letter grade.\"\"\"\n total_weight = 0\n weighted_score = 0\n for check in checks:\n w = GRADE_WEIGHTS.get(check.name, 1)\n score = GRADE_SCORE.get(check.grade, 0)\n weighted_score += score * w\n total_weight += w\n\n pct = int(weighted_score / total_weight) if total_weight else 0\n\n if pct >= 90: letter = 'A'\n elif pct >= 75: letter = 'B'\n elif pct >= 50: letter = 'C'\n elif pct >= 25: letter = 'D'\n else: letter = 'F'\n\n return letter, pct","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 4: Generate Config Fixes","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"def generate_nginx_config(checks: list) -> str:\n \"\"\"Generate a complete nginx add_header block for all failing headers.\"\"\"\n lines = [\"# Security Headers — generated by phy-security-headers\", \"\"]\n\n NGINX_TEMPLATES = {\n 'Content-Security-Policy': (\n \"add_header Content-Security-Policy \"\n \"\\\"default-src 'self'; script-src 'self' 'nonce-REPLACE_WITH_NONCE'; \"\n \"object-src 'none'; base-uri 'self'; frame-ancestors 'none'; \"\n \"upgrade-insecure-requests;\\\" always;\"\n ),\n 'Strict-Transport-Security':\n \"add_header Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\" always;\",\n 'X-Frame-Options':\n \"add_header X-Frame-Options \\\"DENY\\\" always;\",\n 'X-Content-Type-Options':\n \"add_header X-Content-Type-Options \\\"nosniff\\\" always;\",\n 'Referrer-Policy':\n \"add_header Referrer-Policy \\\"strict-origin-when-cross-origin\\\" always;\",\n 'Permissions-Policy':\n \"add_header Permissions-Policy \\\"camera=(), microphone=(), geolocation=(), payment=(), usb=()\\\" always;\",\n 'Cross-Origin-Embedder-Policy':\n \"add_header Cross-Origin-Embedder-Policy \\\"require-corp\\\" always;\",\n 'Cross-Origin-Opener-Policy':\n \"add_header Cross-Origin-Opener-Policy \\\"same-origin\\\" always;\",\n 'Cross-Origin-Resource-Policy':\n \"add_header Cross-Origin-Resource-Policy \\\"same-origin\\\" always;\",\n }\n\n for check in checks:\n if check.grade in ('F', 'C', 'MISSING') and check.name in NGINX_TEMPLATES:\n lines.append(NGINX_TEMPLATES[check.name])\n\n return '\\n'.join(lines)\n\n\ndef generate_nextjs_config(checks: list) -> str:\n \"\"\"Generate Next.js next.config.js headers() array.\"\"\"\n lines = [\n \"// next.config.js — Security Headers\",\n \"const securityHeaders = [\",\n ]\n NEXTJS_TEMPLATES = {\n 'Content-Security-Policy': (\n \" { key: 'Content-Security-Policy', \"\n \"value: \\\"default-src 'self'; script-src 'self'; object-src 'none'; \"\n \"base-uri 'self'; frame-ancestors 'none';\\\" },\"\n ),\n 'Strict-Transport-Security':\n \" { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },\",\n 'X-Frame-Options':\n \" { key: 'X-Frame-Options', value: 'DENY' },\",\n 'X-Content-Type-Options':\n \" { key: 'X-Content-Type-Options', value: 'nosniff' },\",\n 'Referrer-Policy':\n \" { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\",\n 'Permissions-Policy':\n \" { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },\",\n }\n for check in checks:\n if check.grade in ('F', 'C', 'MISSING') and check.name in NEXTJS_TEMPLATES:\n lines.append(NEXTJS_TEMPLATES[check.name])\n lines.extend([\n \"];\",\n \"\",\n \"module.exports = {\",\n \" async headers() {\",\n \" return [{ source: '/(.*)', headers: securityHeaders }];\",\n \" },\",\n \"};\",\n ])\n return '\\n'.join(lines)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 5: Output Report","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"## Security Headers Audit\nURL: https://myapp.com | Checked: 2026-03-19 09:22 UTC\n\n---\n\n### Overall Grade: C (54/100)\n\n| Header | Grade | Status | Key Issue |\n|--------|-------|--------|-----------|\n| Content-Security-Policy | F | 🔴 FAIL | 'unsafe-inline' in script-src |\n| Strict-Transport-Security | B | 🟡 REVIEW | max-age=86400 too short, no preload |\n| X-Frame-Options | A | ✅ PASS | DENY |\n| X-Content-Type-Options | A | ✅ PASS | nosniff |\n| Referrer-Policy | C | 🟠 WARN | Missing |\n| Permissions-Policy | C | 🟠 WARN | Missing |\n| Cross-Origin-Embedder-Policy | C | 🟠 WARN | Missing |\n| Cross-Origin-Opener-Policy | C | 🟠 WARN | Missing |\n\n---\n\n### 🔴 CRITICAL — Content-Security-Policy\n\n**Found:** `default-src 'self' 'unsafe-inline' https://cdn.example.com`\n\n**Issue 1:** `'unsafe-inline'` in default-src — completely negates XSS protection.\nAny injected `\u003cscript>` will execute. This is as bad as no CSP.\n\n**Fix:**\n```html\n\u003c!-- Replace unsafe-inline with nonce-based CSP -->\n\u003c!-- In your HTML template, add nonce to every script tag: -->\n\u003cscript nonce=\"{{CSP_NONCE}}\">...\u003c/script>\n\n\u003c!-- Set header with matching nonce: -->\nContent-Security-Policy: default-src 'self';\n script-src 'self' 'nonce-ABC123' 'strict-dynamic';\n object-src 'none';\n base-uri 'self';\n frame-ancestors 'none';","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Express.js nonce middleware:\nimport secrets\napp.use((req, res, next) => {\n res.locals.nonce = secrets.token_hex(16)\n res.setHeader('Content-Security-Policy',\n `script-src 'nonce-${res.locals.nonce}' 'strict-dynamic'`)\n next()\n})","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"🟡 REVIEW — Strict-Transport-Security","type":"text"}]},{"type":"paragraph","content":[{"text":"Found:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"max-age=86400","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Issue:","type":"text","marks":[{"type":"strong"}]},{"text":" max-age=86400 (1 day) — too short for HSTS preload eligibility (minimum 1 year). Browser will stop enforcing HTTPS after 24 hours if a user hasn't visited recently.","type":"text"}]},{"type":"paragraph","content":[{"text":"Fix:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"nginx"},"content":[{"text":"add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;","type":"text"}]},{"type":"paragraph","content":[{"text":"Submit to preload list:","type":"text","marks":[{"type":"strong"}]},{"text":" After fixing → https://hstspreload.org","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"🟠 MISSING — Referrer-Policy","type":"text"}]},{"type":"paragraph","content":[{"text":"Fix:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"nginx"},"content":[{"text":"add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Generated Config Fixes","type":"text"}]},{"type":"paragraph","content":[{"text":"nginx (add to server block):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"nginx"},"content":[{"text":"# Security Headers — generated by phy-security-headers\nadd_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'nonce-REPLACE_WITH_NONCE'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; upgrade-insecure-requests;\" always;\nadd_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\nadd_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\nadd_header Permissions-Policy \"camera=(), microphone=(), geolocation=(), payment=(), usb=()\" always;\nadd_header Cross-Origin-Embedder-Policy \"require-corp\" always;\nadd_header Cross-Origin-Opener-Policy \"same-origin\" always;","type":"text"}]},{"type":"paragraph","content":[{"text":"Verify after deploy:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -sI https://myapp.com | grep -i -E \"content-security|strict-transport|x-frame|x-content|referrer|permissions\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"Mozilla Observatory Equivalent Score","type":"text"}]},{"type":"paragraph","content":[{"text":"This audit maps to Mozilla Observatory (observatory.mozilla.org) scoring:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CSP with unsafe-inline: -25 pts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HSTS max-age \u003c 6 months: -5 pts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing Referrer-Policy: -5 pts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing Permissions-Policy: -5 pts","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Estimated Observatory Score: ~45/100 (D)","type":"text","marks":[{"type":"strong"}]},{"text":" Fix the CSP first — it alone accounts for 25 points.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"\n---\n\n## Quick Mode Output\n","type":"text"}]},{"type":"paragraph","content":[{"text":"Security Headers: https://myapp.com","type":"text"}]},{"type":"paragraph","content":[{"text":"Overall: C (54/100)","type":"text"}]},{"type":"paragraph","content":[{"text":"🔴 CSP: F — 'unsafe-inline' negates XSS protection (fix: nonce-based CSP) 🟡 HSTS: B — max-age=86400 too short (fix: 31536000 + preload) ✅ X-Frame-Options: A — DENY ✅ X-Content-Type-Options: A — nosniff 🟠 Referrer-Policy: MISSING 🟠 Permissions-Policy: MISSING 🟠 COEP/COOP: MISSING","type":"text"}]},{"type":"paragraph","content":[{"text":"Priority fix: Replace 'unsafe-inline' with nonce → lifts score to A (89/100)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"---","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"phy-security-headers","author":"@skillopedia","source":{"stars":2012,"repo_name":"openclaw-master-skills","origin_url":"https://github.com/leoyeai/openclaw-master-skills/blob/HEAD/skills/phy-security-headers/SKILL.md","repo_owner":"leoyeai","body_sha256":"8ea7a2252ece1a46fafc3ed211dca31f9aae54b20e8a0567258154c6bf9685bb","cluster_key":"a84913e780ee5c2a5ee23dbc86d2d043d4d0a83fcd09a99fb7a2fa33aed90f14","clean_bundle":{"format":"clean-skill-bundle-v1","source":"leoyeai/openclaw-master-skills/skills/phy-security-headers/SKILL.md","attachments":[{"id":"cfbf489e-a9ea-5e18-90ff-408329554827","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cfbf489e-a9ea-5e18-90ff-408329554827/attachment.json","path":"_meta.json","size":292,"sha256":"48a55a6eaf01e46780d34eda31d9dce9f8fee03029bfdd5c5bc84944d38cbe2d","contentType":"application/json; charset=utf-8"}],"bundle_sha256":"500833514e66b8b490db05c6c337744d1ec326f8cc29d79c552305cbd8e780f3","attachment_count":1,"text_attachments":1,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/phy-security-headers/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"Apache-2.0","version":"v1","category":"security","metadata":{"tags":["security","http-headers","csp","hsts","owasp","web-security","developer-tools","frontend","devops","compliance"],"author":"PHY041","version":"1.0.0"},"import_tag":"clean-skills-v1","description":"HTTP security header auditor that fetches response headers from any URL and grades them against OWASP, Mozilla Observatory, and Google standards. Checks Content-Security-Policy (detects unsafe-inline, wildcard sources, missing directives), HSTS (max-age, includeSubDomains, preload), X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CORP/COEP/COOP isolation headers. Assigns A/B/C/F grade per header, generates a one-command nginx/Apache/Next.js fix for every gap. Works against any live URL or local dev server via curl. Zero external cloud account. Triggers on \"security headers\", \"CSP audit\", \"HSTS missing\", \"clickjacking\", \"content security policy\", \"mozilla observatory\", \"/sec-headers\"."}},"renderedAt":1782979511349}

Security Headers Auditor Your site passes all the security scanners until someone iframes it, injects a script through an open CDN source in your CSP, or steals credentials from a page with no HSTS preload. This skill fetches your response headers, grades each security header against OWASP and Mozilla Observatory standards, and gives you the exact config line to add to nginx, Apache, Next.js, or Cloudflare Workers. Works against any URL via curl. Zero external API. --- Trigger Phrases - "security headers", "check my headers", "http headers audit" - "CSP audit", "content security policy" - "HS…