appsec-owasp
Use this skill when securing web applications, preventing OWASP Top 10 vulnerabilities, implementing input validation, or designing authentication. Triggers on XSS, SQL injection, CSRF, SSRF, broken authentication, security headers, input validation, output encoding, OWASP, and any task requiring application security hardening.
engineering securityowaspxsssql-injectionauthenticationappsecWhat is appsec-owasp?
Use this skill when securing web applications, preventing OWASP Top 10 vulnerabilities, implementing input validation, or designing authentication. Triggers on XSS, SQL injection, CSRF, SSRF, broken authentication, security headers, input validation, output encoding, OWASP, and any task requiring application security hardening.
appsec-owasp
appsec-owasp is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Securing web applications, preventing OWASP Top 10 vulnerabilities, implementing input validation, or designing authentication.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill appsec-owasp- The appsec-owasp skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
A practitioner's guide to application security based on the OWASP Top 10 2021. This skill covers the full lifecycle of web application security - from threat modeling to concrete code patterns for preventing injection, authentication failures, XSS, CSRF, SSRF, and misconfiguration. Designed for developers who need security guidance at the code level, not just as policy.
Tags
security owasp xss sql-injection authentication appsec
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair appsec-owasp with these complementary skills:
Frequently Asked Questions
What is appsec-owasp?
Use this skill when securing web applications, preventing OWASP Top 10 vulnerabilities, implementing input validation, or designing authentication. Triggers on XSS, SQL injection, CSRF, SSRF, broken authentication, security headers, input validation, output encoding, OWASP, and any task requiring application security hardening.
How do I install appsec-owasp?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill appsec-owasp in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support appsec-owasp?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
AppSec - OWASP Top 10
A practitioner's guide to application security based on the OWASP Top 10 2021. This skill covers the full lifecycle of web application security - from threat modeling to concrete code patterns for preventing injection, authentication failures, XSS, CSRF, SSRF, and misconfiguration. Designed for developers who need security guidance at the code level, not just as policy.
When to use this skill
Trigger this skill when the user:
- Asks how to prevent XSS, SQL injection, CSRF, or SSRF
- Implements or reviews authentication / session management
- Sets security headers (CSP, HSTS, X-Frame-Options, etc.)
- Validates or sanitizes user input
- Designs authorization logic or access controls
- Reviews code for OWASP Top 10 vulnerabilities
- Asks about output encoding, parameterized queries, or allowlists
Do NOT trigger this skill for:
- Network-level security (firewalls, VPNs, DDoS mitigation) - use a network security skill instead
- Secrets management / key rotation workflows - use a secrets management skill for those operational concerns
Key principles
Never trust user input - All data from the outside world is untrusted: HTTP bodies, headers, query params, cookies, uploaded files, and even data read back from your own database that originated from user input.
Defense in depth - Apply multiple independent security controls. If one layer fails, the next one stops the attack. Never rely on a single control.
Least privilege - Every component (user accounts, DB connections, API tokens, OS processes) should have only the permissions required and nothing more. Blast radius is limited by privilege scope.
Fail securely - When something goes wrong, default to the most restrictive outcome. Deny access on error, not grant it. Surface a generic error message to users, log the detail server-side.
Security by default - Secure configuration should be the default state. Developers should have to explicitly opt out of security controls, not opt in.
Core concepts
OWASP Top 10 2021
| Rank | Category | Root cause | Typical impact |
|---|---|---|---|
| A01 | Broken Access Control | Missing server-side checks, IDOR | Data breach, privilege escalation |
| A02 | Cryptographic Failures | Weak algorithms, missing TLS, plain-text PII | Data exposure, credential theft |
| A03 | Injection (SQL, NoSQL, OS, LDAP) | String-concatenated queries | Data breach, RCE, data destruction |
| A04 | Insecure Design | No threat model, missing abuse cases | Business logic bypass |
| A05 | Security Misconfiguration | Defaults unchanged, debug on in prod | Information disclosure, RCE |
| A06 | Vulnerable and Outdated Components | Unpinned deps, no CVE scanning | Range from XSS to full compromise |
| A07 | Identification and Auth Failures | Weak passwords, no MFA, bad session mgmt | Account takeover |
| A08 | Software and Data Integrity Failures | Unsigned artifacts, insecure deserialization | Supply chain attack, RCE |
| A09 | Security Logging and Monitoring Failures | No audit trail, no alerting | Undetected breach, slow response |
| A10 | SSRF | User-controlled URLs fetched server-side | Internal network access, cloud metadata theft |
Threat modeling basics
Before writing security controls, answer four questions:
- What are we building? - Draw a data-flow diagram including trust boundaries
- What can go wrong? - Use STRIDE (Spoofing, Tampering, Repudiation, Info Disclosure, Denial of Service, Elevation of Privilege)
- What are we going to do about it? - For each threat, decide: mitigate, accept, transfer, or eliminate
- Did we do a good enough job? - Validate controls cover identified threats
Run threat modeling at design time, not after the code is written.
Security headers quick reference
| Header | Recommended value | Defends against |
|---|---|---|
Content-Security-Policy |
default-src 'self'; script-src 'self' |
XSS via inline scripts and external resources |
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
Protocol downgrade, cookie hijacking |
X-Content-Type-Options |
nosniff |
MIME-type confusion attacks |
X-Frame-Options |
DENY |
Clickjacking |
Referrer-Policy |
strict-origin-when-cross-origin |
Referrer leakage |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Browser feature misuse |
See references/security-headers.md for full CSP directive reference and
frame-ancestors vs X-Frame-Options comparison.
Common tasks
Prevent XSS with output encoding
Never insert untrusted data into HTML without context-aware encoding. The encoding rule depends on where in the HTML the data lands.
import DOMPurify from 'dompurify';
import { escape } from 'html-escaper';
// 1. HTML context - escape <, >, &, ", '
function renderComment(userInput: string): string {
return escape(userInput); // safe: <script> not executed
}
// 2. When you must allow some HTML (e.g. rich text) - sanitize, don't escape
function renderRichText(userHtml: string): string {
// DOMPurify strips disallowed tags/attributes; allowlist only what you need
return DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'title'],
});
}
// 3. JavaScript context - use JSON.stringify, never template-inject
// WRONG: <script>var name = "<%= userInput %>";</script>
// RIGHT:
function inlineJsonData(data: unknown): string {
// JSON.stringify encodes <, >, & to unicode escapes automatically
return `<script>var __DATA__ = ${JSON.stringify(data)};</script>`;
}Set
Content-Security-Policy: default-src 'self'; script-src 'self'so that even if encoding fails, inline scripts are blocked by the browser.
Prevent SQL injection with parameterized queries
Never concatenate user input into SQL strings. Always use parameterized queries or a safe ORM layer.
import { Pool } from 'pg';
const pool = new Pool();
// WRONG - string interpolation:
// const rows = await pool.query(`SELECT * FROM users WHERE email = '${email}'`);
// RIGHT - parameterized ($1, $2 for pg):
async function findUserByEmail(email: string) {
const { rows } = await pool.query(
'SELECT id, name, email FROM users WHERE email = $1',
[email]
);
return rows[0] ?? null;
}
// RIGHT - ORM (Prisma example):
// const user = await prisma.user.findUnique({ where: { email } });
// Dynamic ORDER BY (column names can't be parameterized - use an allowlist):
const ALLOWED_SORT_COLUMNS = new Set(['name', 'created_at', 'email'] as const);
async function listUsers(sortBy: string, order: 'ASC' | 'DESC') {
if (!ALLOWED_SORT_COLUMNS.has(sortBy as any)) {
throw new Error(`Invalid sort column: ${sortBy}`);
}
const direction = order === 'DESC' ? 'DESC' : 'ASC'; // only two valid values
const { rows } = await pool.query(
`SELECT id, name FROM users ORDER BY ${sortBy} ${direction}`
);
return rows;
}Implement CSRF protection
For detailed CSRF token pattern and SameSite cookie implementations, see references/auth-csrf-patterns.md.
Set security headers (CSP, HSTS, X-Frame-Options)
import helmet from 'helmet';
import { Express } from 'express';
function applySecurityHeaders(app: Express): void {
app.use(
helmet({
// HSTS: force HTTPS for 2 years, include subdomains, add to preload list
hsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
// CSP: restrict resource loading to same origin; tighten per-app
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // no inline scripts, no eval
styleSrc: ["'self'", "'unsafe-inline'"], // relax only if needed
imgSrc: ["'self'", 'data:', 'https://cdn.example.com'],
connectSrc: ["'self'", 'https://api.example.com'],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"], // replaces X-Frame-Options
upgradeInsecureRequests: [],
},
},
// Clickjacking: frameAncestors in CSP is preferred; keep this as fallback
frameguard: { action: 'deny' },
// Prevent MIME sniffing
noSniff: true,
// Limit referrer leakage
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Disable browser features not used by the app
permittedCrossDomainPolicies: false,
})
);
// Permissions-Policy (not yet in helmet stable - set manually)
app.use((_req, res, next) => {
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=()'
);
next();
});
}Implement secure authentication (bcrypt, JWT, session)
For detailed bcrypt password hashing, JWT issuance, and secure login handler implementations, see references/auth-csrf-patterns.md.
Prevent SSRF
Validate and restrict any URL your server fetches on behalf of a user request.
import { URL } from 'url';
import dns from 'dns/promises';
import { isPrivate } from 'private-ip'; // npm i private-ip
const ALLOWED_SCHEMES = new Set(['https:']);
const ALLOWED_HOSTS = new Set(['api.example.com', 'cdn.example.com']);
async function isSafeUrl(rawUrl: string): Promise<boolean> {
let parsed: URL;
try {
parsed = new URL(rawUrl);
} catch {
return false; // not a valid URL
}
// 1. Allowlist scheme
if (!ALLOWED_SCHEMES.has(parsed.protocol)) return false;
// 2. If you can't use a host allowlist, at least block private/internal ranges
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
// Resolve the hostname and check its IP
try {
const addresses = await dns.lookup(parsed.hostname, { all: true });
for (const { address } of addresses) {
if (isPrivate(address)) return false; // blocks 10.x, 172.16-31.x, 192.168.x, 127.x, etc.
}
} catch {
return false; // DNS resolution failure - deny
}
}
return true;
}
async function fetchWebhook(userProvidedUrl: string, payload: unknown) {
if (!(await isSafeUrl(userProvidedUrl))) {
throw new Error('URL not allowed');
}
// Proceed with fetch - also set a tight timeout
const res = await fetch(userProvidedUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(5000), // 5-second hard timeout
});
return res;
}Input validation with allowlists
Reject anything that doesn't match your expected format. Allowlists are far safer than blocklists because attackers find encodings you didn't block.
import { z } from 'zod'; // npm i zod
// Define strict schemas - unknown fields are stripped by default
const CreateUserSchema = z.object({
email: z.string().email().max(254).toLowerCase(),
name: z.string().min(1).max(100).regex(/^[\p{L}\p{N} '-]+$/u), // letters, digits, space, hyphen, apostrophe
role: z.enum(['viewer', 'editor', 'admin']), // strict allowlist, not a free string
age: z.number().int().min(13).max(120).optional(),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
function validateCreateUser(body: unknown): CreateUserInput {
// parse() throws ZodError with field-level detail on failure
return CreateUserSchema.parse(body);
}
// Use in Express middleware
import { Request, Response, NextFunction } from 'express';
function validateBody<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
error: 'Validation failed',
issues: result.error.flatten().fieldErrors,
});
return;
}
req.body = result.data; // replace with validated + stripped data
next();
};
}
// router.post('/users', validateBody(CreateUserSchema), createUserHandler);Anti-patterns
| Anti-pattern | Why it's dangerous | What to do instead |
|---|---|---|
| String-concatenating SQL | Allows injection; attacker can terminate the query and append arbitrary SQL | Always use parameterized queries or ORM bind parameters |
| Storing passwords as MD5/SHA-256 | Fast hashes are brute-forceable; rainbow tables precomputed | Use bcrypt (cost 12+) or Argon2id |
| Putting JWT in localStorage | XSS can read localStorage and steal the token | Store JWT in httpOnly, Secure, SameSite cookie |
| Reflecting the Origin header in CORS | Equivalent to Access-Control-Allow-Origin: * with no audit trail |
Maintain an explicit allowlist of allowed origins |
| Using blocklists for input validation | Encodings, Unicode variants, and novel payloads bypass blocklists | Use allowlists - define exactly what is valid and reject everything else |
| Fetching user-supplied URLs without validation | SSRF: attacker reaches internal services, cloud metadata endpoint (169.254.169.254) | Validate scheme, resolve DNS, reject private IP ranges; prefer a host allowlist |
Gotchas
DNS rebinding bypasses IP-based SSRF blocklists - An attacker registers a domain that initially resolves to a public IP (passing your IP check), then immediately re-resolves to
169.254.169.254(cloud metadata). The server fetches the attacker's internal target. Mitigate by using a host allowlist, not just an IP blocklist, or by caching the resolved IP and using it for the actual connection.bcrypt.compare()must always run even for missing users - If you return early with "user not found" before callingbcrypt.compare(), the response time is measurably shorter than a failed password check. Timing-based enumeration reveals valid email addresses. Always runbcrypt.compare()against a dummy hash even when the user doesn't exist.CSP
unsafe-inlineonscript-srcnegates XSS protection - Adding'unsafe-inline'toscript-srcallows all inline scripts, which is what CSP exists to prevent. If you need inline styles, use'unsafe-inline'onstyle-srconly. For inline scripts, use nonces or hashes instead.SameSite=Laxdoesn't protect non-GET state-changing requests on cross-site navigation - Top-level navigations with GET are allowed underSameSite=Lax. For mutation endpoints invoked via form POST from another origin,Laxprovides no protection. UseSameSite=Strictor implement CSRF tokens for server-rendered form submissions.Dynamic
ORDER BYcolumn names can't be parameterized and are injection vectors - You can't use$1for a column name or SQL keyword. AsortByquery parameter passed directly intoORDER BY ${sortBy}is injectable. Always validate against an explicit allowlist of permitted column names before interpolating.
References
For deeper implementation guidance, load the relevant reference file:
references/security-headers.md- Full CSP directive reference, HSTS preloading, frame-ancestors vs X-Frame-Options, Permissions-Policyreferences/auth-csrf-patterns.md- CSRF token pattern, SameSite cookie setup, bcrypt/JWT authentication implementation
References
auth-csrf-patterns.md
CSRF Protection and Secure Authentication Patterns
Implement CSRF protection
Use the Synchronizer Token Pattern or SameSite cookies. For modern SPAs the
SameSite=Strict or SameSite=Lax cookie attribute is usually sufficient.
import crypto from 'crypto';
import { Request, Response, NextFunction } from 'express';
// --- Token pattern (for traditional server-rendered forms) ---
function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex');
}
function setCsrfToken(req: Request, res: Response): string {
const token = generateCsrfToken();
// Store in httpOnly session, expose to page via non-httpOnly cookie or meta tag
req.session.csrfToken = token;
return token;
}
function verifyCsrf(req: Request, res: Response, next: NextFunction): void {
const sessionToken = req.session?.csrfToken;
const submittedToken =
(req.headers['x-csrf-token'] as string) ?? req.body?._csrf;
if (
!sessionToken ||
!submittedToken ||
!crypto.timingSafeEqual(
Buffer.from(sessionToken),
Buffer.from(submittedToken)
)
) {
res.status(403).json({ error: 'Invalid CSRF token' });
return;
}
next();
}
// --- SameSite cookies (for SPAs with JWT or session cookies) ---
// Set on login response:
res.cookie('session', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // never sent on cross-site requests
path: '/',
});Implement secure authentication (bcrypt, JWT, session)
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Request, Response } from 'express';
const BCRYPT_ROUNDS = 12; // increase as hardware improves
const JWT_SECRET = process.env.JWT_SECRET!; // loaded from secrets manager
const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL = '7d';
// --- Password hashing ---
async function hashPassword(plain: string): Promise<string> {
return bcrypt.hash(plain, BCRYPT_ROUNDS);
}
async function verifyPassword(plain: string, hash: string): Promise<boolean> {
return bcrypt.compare(plain, hash);
}
// --- JWT issuance ---
interface TokenPayload {
sub: string; // user ID
role: string;
}
function issueAccessToken(payload: TokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_TTL });
}
// --- Secure login handler ---
async function login(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
const user = await findUserByEmail(email);
// Always run bcrypt even on missing user - prevent timing-based user enumeration
const hash = user?.passwordHash ?? '$2b$12$invalidhashpadding000000000000000000000000000000000000';
const valid = await verifyPassword(password, hash);
if (!user || !valid) {
res.status(401).json({ error: 'Invalid email or password' }); // generic message
return;
}
const accessToken = issueAccessToken({ sub: user.id, role: user.role });
// Store access token in httpOnly cookie - not localStorage
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes in ms
});
res.json({ ok: true });
} security-headers.md
Security Headers Reference
Complete guide to HTTP security headers for web applications. Each header is described with its purpose, recommended value, common pitfalls, and browser support notes.
1. Content-Security-Policy (CSP)
The most powerful and complex security header. CSP instructs the browser on which sources are allowed to load scripts, styles, images, fonts, and other resources. A well-configured CSP neutralizes most XSS attacks even if output encoding fails.
Directives
| Directive | Controls | Recommended value |
|---|---|---|
default-src |
Fallback for all fetch directives not explicitly set | 'self' |
script-src |
JavaScript sources | 'self' (add 'nonce-{random}' for inline scripts) |
style-src |
CSS sources | 'self' (add 'unsafe-inline' only if inline styles are unavoidable) |
img-src |
Image sources | 'self' data: https://cdn.example.com |
font-src |
Font sources | 'self' https://fonts.gstatic.com |
connect-src |
XHR, fetch, WebSocket endpoints | 'self' https://api.example.com |
frame-src |
<frame> and <iframe> src |
'none' unless embedding content |
frame-ancestors |
Which origins can embed THIS page | 'none' or 'self' |
object-src |
<object>, <embed>, <applet> |
'none' - Flash is dead |
base-uri |
<base> href values |
'self' - prevents base tag hijacking |
form-action |
Where forms can submit | 'self' |
upgrade-insecure-requests |
Auto-upgrade HTTP to HTTPS | Include (no value) |
block-all-mixed-content |
Block HTTP resources on HTTPS pages | Include if not using upgrade-insecure-requests |
report-uri (legacy) |
Where to POST violation reports | https://your-csp-endpoint.example.com/report |
report-to |
Modern reporting API endpoint | JSON endpoint name |
Starter policy (strict)
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;Nonce-based inline scripts (preferred over unsafe-inline)
When your app requires inline scripts, use a per-request nonce instead of
'unsafe-inline'. The nonce must be cryptographically random and change on
every page load.
import crypto from 'crypto';
import { Request, Response, NextFunction } from 'express';
function cspMiddleware(req: Request, res: Response, next: NextFunction) {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self';`
);
next();
}
// In your template: <script nonce="<%= nonce %>">...</script>CSP report-only mode (for incremental rollout)
Deploy CSP in report-only mode first to discover violations without breaking production. Migrate to enforcing once violations are resolved.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportCommon CSP mistakes
| Mistake | Impact | Fix |
|---|---|---|
script-src 'unsafe-inline' |
Defeats XSS protection entirely | Use nonces or hashes for inline scripts |
script-src * or script-src https: |
Any HTTPS script source is allowed (includes attacker-controlled CDNs) | Enumerate exact domains |
Missing object-src 'none' |
Flash/Java plugins can bypass CSP | Always include object-src 'none' |
Missing base-uri 'self' |
Attacker injects <base href> to hijack relative URLs |
Always include base-uri 'self' |
'unsafe-eval' in script-src |
Allows eval(), setTimeout(string), new Function() |
Refactor code to eliminate eval; required if using some legacy libraries |
2. Strict-Transport-Security (HSTS)
Forces browsers to use HTTPS for all future requests to the domain. Once set, the browser ignores HTTP responses and goes directly to HTTPS for the configured duration.
Recommended value
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload| Parameter | Meaning |
|---|---|
max-age=63072000 |
Cache HSTS policy for 2 years (in seconds) |
includeSubDomains |
Apply to all subdomains - include only if all subdomains serve HTTPS |
preload |
Request inclusion in browser HSTS preload lists (see hstspreload.org) |
Rollout strategy
Start with a short max-age and expand gradually:
# Week 1 - test
Strict-Transport-Security: max-age=300
# Week 2 - expand
Strict-Transport-Security: max-age=86400
# Week 4 - include subdomains
Strict-Transport-Security: max-age=86400; includeSubDomains
# Week 8 - full production value
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadWarning: Only set max-age values over a week when you are confident that
all endpoints - including subdomains - serve valid HTTPS. HSTS errors cannot
be overridden by end users (by design). A misconfigured HSTS + expired cert
will lock users out for the duration of max-age.
3. X-Frame-Options vs frame-ancestors
Both protect against clickjacking (embedding your page in an <iframe> to
trick users into clicking hidden elements).
| Header | Spec status | Values | Notes |
|---|---|---|---|
X-Frame-Options |
Obsolete but widely supported | DENY, SAMEORIGIN |
Cannot specify multiple origins |
frame-ancestors (CSP directive) |
Current standard | 'none', 'self', specific URIs |
Supersedes X-Frame-Options; more flexible |
Recommendation
Use frame-ancestors in your CSP and keep X-Frame-Options as a fallback for
older browsers.
# CSP directive (preferred)
Content-Security-Policy: frame-ancestors 'none';
# Fallback header
X-Frame-Options: DENYWhen frame-ancestors and X-Frame-Options both exist, modern browsers honor
frame-ancestors and ignore X-Frame-Options.
| Value | Effect |
|---|---|
frame-ancestors 'none' |
Page cannot be embedded anywhere |
frame-ancestors 'self' |
Page can only be embedded by same origin |
frame-ancestors https://dashboard.example.com |
Specific allowed parent |
4. X-Content-Type-Options
Prevents browsers from MIME-sniffing a response away from the declared Content-Type. Without this header, a browser might execute an uploaded text file as JavaScript if it looks like a script.
X-Content-Type-Options: nosniffnosniff is the only valid value. Always set it. There is no reason not to.
5. Referrer-Policy
Controls how much referrer information is sent in the Referer header when
navigating away from your site. Without this header, full URLs (including query
params with tokens or PII) are leaked to third-party sites.
| Value | What is sent | When to use |
|---|---|---|
no-referrer |
Nothing | Maximum privacy; breaks some analytics |
no-referrer-when-downgrade |
Full URL to HTTPS, nothing to HTTP | Browser default (pre-2021); avoid |
origin |
Just the origin (e.g., https://example.com) |
When downstream needs origin but not path |
strict-origin |
Origin on same security level, nothing on downgrade | Good for public pages |
origin-when-cross-origin |
Full URL same-origin, origin cross-origin | Common choice |
strict-origin-when-cross-origin |
Full URL same-origin, origin cross-origin, nothing on HTTPS->HTTP | Recommended default |
unsafe-url |
Full URL everywhere | Never use |
Referrer-Policy: strict-origin-when-cross-origin6. Permissions-Policy (formerly Feature-Policy)
Restricts which browser features the page and embedded iframes can use. Disabling unused features reduces the attack surface from malicious scripts.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()Syntax: feature=() disables entirely; feature=(self) allows only same-origin;
feature=(self "https://trusted.com") allows same-origin and specific third party.
| Feature | Recommendation |
|---|---|
camera |
() unless you build video chat |
microphone |
() unless voice features are needed |
geolocation |
() unless location features are used |
payment |
() unless you use Payment Request API |
usb |
() unless you interface with USB devices |
interest-cohort |
() - opt out of FLoC/Topics API tracking |
7. Cross-Origin Headers (CORP, COEP, COOP)
These three headers work together to provide stronger process isolation and are
required for features like SharedArrayBuffer and high-resolution timers.
Cross-Origin-Resource-Policy (CORP)
Cross-Origin-Resource-Policy: same-originPrevents other origins from loading this resource. Values: same-origin,
same-site, cross-origin.
Cross-Origin-Embedder-Policy (COEP)
Cross-Origin-Embedder-Policy: require-corpRequires that all cross-origin resources opt in to being loaded (via CORP or
CORS). Required to enable SharedArrayBuffer.
Cross-Origin-Opener-Policy (COOP)
Cross-Origin-Opener-Policy: same-originIsolates the browsing context group, preventing cross-origin window references. Mitigates Spectre-class side-channel attacks.
To enable SharedArrayBuffer (e.g., for WebAssembly workloads):
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin8. Express.js - Full secure header configuration
import helmet from 'helmet';
import { Express } from 'express';
import crypto from 'crypto';
export function applySecurityHeaders(app: Express): void {
// Generate a per-request nonce for inline scripts
app.use((_req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
(_req, res) => `'nonce-${(res as any).locals.cspNonce}'`,
],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
hsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' },
})
);
app.use((_req, res, next) => {
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'
);
next();
});
}9. Testing your headers
| Tool | How to use |
|---|---|
| securityheaders.com | Paste your URL, get a grade and per-header breakdown |
| Mozilla Observatory | Comprehensive scan including TLS, cookies, and CORS |
curl -I https://yoursite.com |
Quick CLI check of response headers |
| Chrome DevTools > Network > Response Headers | Inspect headers for any page load |
| OWASP ZAP passive scan | Automated header check as part of DAST scanning |
Minimum passing checklist
-
Content-Security-Policyset and does not containunsafe-inlinein script-src -
Strict-Transport-Securitywithmax-age>= 31536000 -
X-Content-Type-Options: nosniff -
X-Frame-Options: DENYorframe-ancestors 'none'in CSP -
Referrer-Policyset to anything other thanunsafe-urlor browser default -
Permissions-Policyrestricts at minimum camera, microphone, geolocation - No
ServerorX-Powered-Byheaders leaking technology stack info
Frequently Asked Questions
What is appsec-owasp?
Use this skill when securing web applications, preventing OWASP Top 10 vulnerabilities, implementing input validation, or designing authentication. Triggers on XSS, SQL injection, CSRF, SSRF, broken authentication, security headers, input validation, output encoding, OWASP, and any task requiring application security hardening.
How do I install appsec-owasp?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill appsec-owasp in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support appsec-owasp?
appsec-owasp works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.