A Cloudflare Worker that analyzes HTTP security headers using Mozilla Observatory methodology. Get instant letter grades (A+ through F) for any website's security posture.
Live Demo: fedlin.com — Try the scanner on the homepage.
- Mozilla Observatory Scoring — Exact methodology match for consistent grades
- Comprehensive Analysis — CSP, HSTS, X-Frame-Options, and more
- Subresource Integrity — Checks external scripts for
integrityattributes - Redirect Analysis — Validates HTTP → HTTPS redirect behavior
- Zero Dependencies — Pure Cloudflare Worker, no external packages
- CORS Enabled — Use from any frontend application
# Install Wrangler CLI
npm install -g wrangler
# Clone this repo
git clone https://github.com/fedlinllc/security-headers-scanner.git
cd security-headers-scanner
# Login to Cloudflare
wrangler login
# Deploy
wrangler deployYour scanner will be live at: https://security-headers-scanner.<your-subdomain>.workers.dev
- Go to Cloudflare Dashboard → Workers & Pages
- Click Create Application → Create Worker
- Name it
security-headers-scanner - Click Deploy, then Edit Code
- Paste the contents of
src/index.js - Click Save and Deploy
curl "https://your-worker.workers.dev/?url=https://example.com"{
"success": true,
"url": "https://example.com",
"finalUrl": "https://example.com/",
"statusCode": 200,
"score": 85,
"maxScore": 135,
"grade": "A-",
"gradeColor": "green",
"gradeLabel": "Good",
"passed": 6,
"warnings": 1,
"missing": 2,
"total": 10,
"headers": [
{
"name": "Content-Security-Policy",
"key": "content-security-policy",
"status": "present",
"value": "default-src 'self'",
"scoreImpact": "Pass",
"notes": null
}
// ... more headers
],
"rawHeaders": {
"content-security-policy": "default-src 'self'",
"strict-transport-security": "max-age=31536000"
// ... all response headers
}
}Scoring matches Mozilla Observatory:
| Grade | Score Range | Label |
|---|---|---|
| A+ | 100+ | Excellent |
| A | 90-99 | Very Good |
| A- | 85-89 | Good |
| B+ | 80-84 | Above Average |
| B | 70-79 | Average |
| B- | 65-69 | Below Average |
| C+ | 60-64 | Fair |
| C | 50-59 | Needs Work |
| C- | 45-49 | Poor |
| D+ | 40-44 | Bad |
| D | 35-39 | Very Bad |
| D- | 30-34 | Critical |
| F | 0-29 | Failing |
| Header | Missing Penalty | Notes |
|---|---|---|
| Content-Security-Policy | -25 | XSS protection |
| Strict-Transport-Security | -20 | HTTPS enforcement |
| X-Frame-Options | -20 | Clickjacking protection |
| X-Content-Type-Options | -5 | MIME sniffing prevention |
| Referrer-Policy | 0 | +5 bonus for private values |
| Check | Impact |
|---|---|
| Subresource Integrity | -50 if HTTP scripts, -5 if missing SRI, +5 if all pass |
| HTTP→HTTPS Redirect | -5 to -20 for improper redirects |
| Cookie Security | -20 if missing Secure flag on HTTPS |
| Header | Purpose |
|---|---|
Content-Security-Policy |
Prevents XSS and data injection |
Strict-Transport-Security |
Forces HTTPS connections |
X-Frame-Options |
Prevents clickjacking |
X-Content-Type-Options |
Prevents MIME sniffing |
Referrer-Policy |
Controls referrer leakage |
Permissions-Policy |
Restricts browser features |
Cross-Origin-Opener-Policy |
Isolates browsing context |
Cross-Origin-Resource-Policy |
Controls cross-origin loading |
async function scanHeaders(url) {
const response = await fetch(
`https://your-worker.workers.dev/?url=${encodeURIComponent(url)}`
);
const data = await response.json();
console.log(`Grade: ${data.grade} (${data.score}/${data.maxScore})`);
console.log(`Passed: ${data.passed}, Warnings: ${data.warnings}, Missing: ${data.missing}`);
return data;
}
// Usage
scanHeaders('https://example.com').then(console.log);import { useState } from 'react';
function SecurityScanner() {
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const scan = async (url) => {
setLoading(true);
const res = await fetch(`https://your-worker.workers.dev/?url=${encodeURIComponent(url)}`);
setResult(await res.json());
setLoading(false);
};
return (
<div>
<input type="url" id="url" placeholder="https://example.com" />
<button onClick={() => scan(document.getElementById('url').value)}>
{loading ? 'Scanning...' : 'Scan'}
</button>
{result && (
<div>
<h2>Grade: {result.grade}</h2>
<p>Score: {result.score}/{result.maxScore}</p>
</div>
)}
</div>
);
}Add a custom route like api.yourdomain.com/scan:
- In Cloudflare Dashboard → Workers → Your Worker → Triggers
- Add a Custom Domain or Route
- Update your frontend to use the new URL
# Run locally
wrangler dev
# Test
curl "http://localhost:8787/?url=https://cloudflare.com"- Fork the repo
- Create a feature branch
- Make your changes
- Submit a pull request
- Mozilla Observatory — The scoring methodology we follow
- FEDLIN — Enterprise security consulting (live scanner implementation)
- Cloudflare Workers Docs — Worker platform documentation
MIT License — see LICENSE
Jeremiah Coakley
Principal Security Architect, FEDLIN LLC