skills/security/csp-headers/SKILL.md
Use this skill when the user says 'CSP', 'Content-Security-Policy', 'security headers', 'HSTS', 'X-Frame-Options', 'clickjacking', 'unsafe-inline', 'unsafe-eval', or needs to audit, generate, or fix HTTP security headers for a web application. Do NOT use for API route audits or dependency scanning.
npx skillsauth add cwinvestments/memstack memstack-security-csp-headersInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit existing security headers, identify overly permissive directives, and generate a production-ready Content-Security-Policy with companion headers.
When this skill activates, output:
🛡️ CSP Headers — Auditing your security headers...
| Context | Status | |---------|--------| | User says "CSP", "security headers", "Content-Security-Policy" | ACTIVE | | User wants to fix unsafe-inline, unsafe-eval, or wildcard directives | ACTIVE | | User mentions HSTS, X-Frame-Options, or Permissions-Policy | ACTIVE | | User wants a full OWASP security audit (not just headers) | DORMANT — see owasp-top10 | | User wants to scan for secrets in code | DORMANT — see secrets-scanner | | User wants API-level security (auth, rate limiting) | DORMANT — see api-audit |
Ask the user for:
<script> tags or inline styles?Check for CSP and security headers in all possible locations:
Where to look:
next.config.js (headers() function), nginx.conf, Caddyfile<meta http-equiv="Content-Security-Policy" content="..."> in HTMLvercel.json, netlify.toml, _headers file── EXISTING HEADERS FOUND ─────────────────
Location: [file:line or "not found"]
Content-Security-Policy:
[current policy or "MISSING"]
Strict-Transport-Security:
[current value or "MISSING"]
X-Content-Type-Options:
[current value or "MISSING"]
X-Frame-Options:
[current value or "MISSING"]
Referrer-Policy:
[current value or "MISSING"]
Permissions-Policy:
[current value or "MISSING"]
X-XSS-Protection:
[current value or "MISSING — deprecated but still useful for older browsers"]
If a CSP exists, audit each directive for security issues:
| Directive | Current Value | Issue | Severity | Fix |
|-----------|--------------|-------|----------|-----|
| default-src | * | Allows loading from any origin | 🔴 Critical | Restrict to known origins |
| script-src | 'unsafe-inline' | Allows inline scripts — XSS vector | 🔴 Critical | Use nonces or hashes |
| script-src | 'unsafe-eval' | Allows dynamic code execution — injection risk | 🟡 High | Remove, refactor code |
| style-src | 'unsafe-inline' | Allows inline styles — less severe | 🟠 Medium | Use nonces if feasible |
| img-src | * | Allows images from any origin | 🟠 Medium | Restrict to known CDNs |
| frame-ancestors | missing | No clickjacking protection | 🟡 High | Add 'self' or 'none' |
| connect-src | missing | No restriction on fetch/XHR targets | 🟠 Medium | Restrict to API origins |
| base-uri | missing | Base tag injection possible | 🟡 High | Add 'self' |
| form-action | missing | Forms can submit to any origin | 🟠 Medium | Add 'self' |
| object-src | missing | Plugin content allowed | 🟡 High | Add 'none' |
Common dangerous patterns:
BAD: default-src * → Defeats the entire purpose of CSP
BAD: script-src 'unsafe-inline' → XSS protection nullified
BAD: script-src 'unsafe-eval' → Dynamic code execution permitted
BAD: script-src https: → Any HTTPS origin can serve scripts
BAD: script-src *.googleapis.com → JSONP endpoints can bypass CSP
BAD: connect-src * → Data exfiltration unrestricted
BAD: frame-ancestors not set → Clickjacking possible
Identify all external resources the application loads:
── RESOURCE INVENTORY ─────────────────────
Scripts (script-src):
- https://www.googletagmanager.com — Google Tag Manager
- https://js.stripe.com — Stripe.js
- https://cdn.jsdelivr.net — jsDelivr CDN
- 'self' — Own domain scripts
Styles (style-src):
- https://fonts.googleapis.com — Google Fonts CSS
- 'self' — Own stylesheets
- 'unsafe-inline' — Needed for: [reason]
Fonts (font-src):
- https://fonts.gstatic.com — Google Fonts files
- 'self' — Self-hosted fonts
Images (img-src):
- https://res.cloudinary.com — Cloudinary images
- https://*.githubusercontent.com — GitHub avatars
- data: — Data URI images
- 'self' — Own images
Connections (connect-src):
- https://api.example.com — Own API
- https://api.stripe.com — Stripe API
- https://www.google-analytics.com — Analytics
- 'self' — Same-origin requests
Frames (frame-src):
- https://js.stripe.com — Stripe checkout iframe
- https://www.youtube.com — Embedded videos
- 'none' — If no iframes needed
Build a CSP policy based on actual resource usage:
── RECOMMENDED CSP ────────────────────────
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{SERVER_GENERATED}' https://www.googletagmanager.com https://js.stripe.com;
style-src 'self' 'nonce-{SERVER_GENERATED}' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' https://res.cloudinary.com data:;
connect-src 'self' https://api.stripe.com https://www.google-analytics.com;
frame-src https://js.stripe.com https://www.youtube.com;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requests;
Nonce-based script loading (recommended over 'unsafe-inline'):
// Express/Next.js middleware example
const crypto = require('crypto');
function cspMiddleware(req, res, next) {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data:",
"connect-src 'self'",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
"upgrade-insecure-requests",
].join('; '));
next();
}
<!-- Use nonce in script/style tags -->
<script nonce="<%= cspNonce %>">
// Inline script allowed by nonce
</script>
Hash-based alternative (for static inline scripts):
# Generate hash for a known inline script
echo -n "console.log('hello')" | openssl dgst -sha256 -binary | openssl enc -base64
# Result: 'sha256-xxxxxxxxx'
# Add to CSP: script-src 'sha256-xxxxxxxxx'
CSP alone isn't enough. Generate the full security headers suite:
── COMPLETE SECURITY HEADERS ──────────────
# HSTS — Force HTTPS for all future visits
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# Prevent MIME type sniffing attacks
X-Content-Type-Options: nosniff
# Clickjacking protection (legacy — frame-ancestors in CSP is preferred)
X-Frame-Options: SAMEORIGIN
# Control referrer information leakage
Referrer-Policy: strict-origin-when-cross-origin
# Restrict browser features
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=self
# Legacy XSS filter (deprecated but harmless)
X-XSS-Protection: 0
Header explanations:
| Header | Purpose | Recommended Value |
|--------|---------|-------------------|
| Strict-Transport-Security | Force HTTPS, prevent SSL stripping | max-age=63072000; includeSubDomains; preload |
| X-Content-Type-Options | Prevent MIME sniffing (serving JS as image) | nosniff |
| X-Frame-Options | Prevent clickjacking (legacy, CSP preferred) | SAMEORIGIN or DENY |
| Referrer-Policy | Control referer header leakage | strict-origin-when-cross-origin |
| Permissions-Policy | Disable unused browser APIs | Disable camera, mic, geo unless needed |
| X-XSS-Protection | Legacy XSS filter | 0 (disable — can cause issues, CSP is better) |
Always deploy CSP in report-only mode first to avoid breaking production:
── DEPLOYMENT STRATEGY ────────────────────
Phase 1: REPORT-ONLY (1-2 weeks)
Header: Content-Security-Policy-Report-Only
Purpose: Collect violations without blocking anything
Report endpoint: /api/csp-report
Phase 2: ENFORCE (after zero violations)
Header: Content-Security-Policy
Purpose: Block unauthorized resources
Keep report-uri for ongoing monitoring
Report-only header:
Content-Security-Policy-Report-Only:
[same policy as above];
report-uri /api/csp-report;
report-to csp-endpoint
Violation report handler:
// POST /api/csp-report
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.warn('CSP Violation:', {
blockedUri: report['blocked-uri'],
directive: report['violated-directive'],
documentUri: report['document-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number'],
});
res.status(204).end();
});
Common violations to expect:
data: to img-src if needed)Next.js (next.config.js):
const securityHeaders = [
{ key: 'Content-Security-Policy', value: "default-src 'self'; ..." },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
Express middleware:
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; ...");
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});
Nginx:
add_header Content-Security-Policy "default-src 'self'; ..." always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Caddy:
header {
Content-Security-Policy "default-src 'self'; ..."
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
Vercel (vercel.json):
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "default-src 'self'; ..." },
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
Present the complete security headers audit and configuration:
━━━ SECURITY HEADERS REPORT ━━━━━━━━━━━━━━
Project: [name]
Framework: [framework]
Hosting: [platform]
── AUDIT RESULTS ──────────────────────────
Headers found: [X] of 6
Issues found: [count]
🔴 Critical: [count]
🟡 High: [count]
🟠 Medium: [count]
── ISSUES ─────────────────────────────────
[directive-level audit table]
── RESOURCE INVENTORY ─────────────────────
[categorized external resources]
── RECOMMENDED CSP ────────────────────────
[complete CSP policy]
── COMPANION HEADERS ──────────────────────
[HSTS, X-Content-Type-Options, etc.]
── IMPLEMENTATION ─────────────────────────
[framework-specific config, copy-paste ready]
── DEPLOYMENT PLAN ────────────────────────
Phase 1: Report-only ([duration])
Phase 2: Enforce (after zero violations)
Report endpoint: [handler code]
── SECURITY SCORE ─────────────────────────
Score: [A+ / A / B / C / D / F]
CSP: [pass/fail]
HSTS: [pass/fail]
X-Content-Type: [pass/fail]
X-Frame-Options: [pass/fail]
Referrer-Policy: [pass/fail]
Permissions: [pass/fail]
Grading:
tools
Use when the user says 'save diary', 'log session', 'wrapping up', or at end of a productive session.
tools
Use when the user says 'submit to marketplace', 'publish my skill', 'share this skill', 'list on marketplace', 'submit plugin', 'publish to community', or needs to submit a skill or plugin to a community marketplace via PR. Do NOT use for building skills or writing plugin code.
development
Use when the user says 'write browser tests', 'test this page', 'playwright test', 'e2e test', 'end to end test', 'browser test', 'test the UI', or needs Playwright-based browser testing for a web application. Do NOT use for unit tests, API tests, or non-browser testing.
development
Use when the user says 'teach me', 'explain as you go', 'mentor mode', 'walk me through', 'help me learn', 'explain why', 'learning mode', or wants real-time plain language narration of decisions and tradeoffs while building. Do NOT use for code review or debugging.