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.
Instrucciones de origen · Vista previa de solo lectura
name
appsec-owasp
version
0.1.0
description
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.
When this skill is activated, always start your first response with the 🧢 emoji.
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
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.
importDOMPurifyfrom'dompurify';
import { escape } from'html-escaper';
// 1. HTML context - escape <, >, &, ", 'functionrenderComment(userInput: string): string {
returnescape(userInput); // safe: <script> not executed
}
// 2. When you must allow some HTML (e.g. rich text) - sanitize, don't escapefunctionrenderRichText(userHtml: string): string {
// DOMPurify strips disallowed tags/attributes; allowlist only what you needreturnDOMPurify.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:functioninlineJsonData(data: unknown): string {
// JSON.stringify encodes <, >, & to unicode escapes automaticallyreturn`<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 = newPool();
// WRONG - string interpolation:// const rows = await pool.query(`SELECT * FROM users WHERE email = '${email}'`);// RIGHT - parameterized ($1, $2 for pg):asyncfunctionfindUserByEmail(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):constALLOWED_SORT_COLUMNS = newSet(['name', 'created_at', 'email'] asconst);
asyncfunctionlistUsers(sortBy: string, order: 'ASC' | 'DESC') {
if (!ALLOWED_SORT_COLUMNS.has(sortBy asany)) {
thrownewError(`Invalid sort column: ${sortBy}`);
}
const direction = order === 'DESC' ? 'DESC' : 'ASC'; // only two valid valuesconst { 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';
functionapplySecurityHeaders(app: Express): void {
app.use(
helmet({
// HSTS: force HTTPS for 2 years, include subdomains, add to preload listhsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true,
},
// CSP: restrict resource loading to same origin; tighten per-appcontentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // no inline scripts, no evalstyleSrc: ["'self'", "'unsafe-inline'"], // relax only if neededimgSrc: ["'self'", 'data:', 'https://cdn.example.com'],
connectSrc: ["'self'", 'https://api.example.com'],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"], // replaces X-Frame-OptionsupgradeInsecureRequests: [],
},
},
// Clickjacking: frameAncestors in CSP is preferred; keep this as fallbackframeguard: { action: 'deny' },
// Prevent MIME sniffingnoSniff: true,
// Limit referrer leakagereferrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Disable browser features not used by the apppermittedCrossDomainPolicies: false,
})
);
// Permissions-Policy (not yet in helmet stable - set manually)
app.use((_req, res, next) => {
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=()'
);
next();
});
}
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 calling bcrypt.compare(), the response time is measurably shorter than a failed password check. Timing-based enumeration reveals valid email addresses. Always run bcrypt.compare() against a dummy hash even when the user doesn't exist.
CSP unsafe-inline on script-src negates XSS protection - Adding 'unsafe-inline' to script-src allows all inline scripts, which is what CSP exists to prevent. If you need inline styles, use 'unsafe-inline' on style-src only. For inline scripts, use nonces or hashes instead.
SameSite=Lax doesn't protect non-GET state-changing requests on cross-site navigation - Top-level navigations with GET are allowed under SameSite=Lax. For mutation endpoints invoked via form POST from another origin, Lax provides no protection. Use SameSite=Strict or implement CSRF tokens for server-rendered form submissions.
Dynamic ORDER BY column names can't be parameterized and are injection vectors - You can't use $1 for a column name or SQL keyword. A sortBy query parameter passed directly into ORDER 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-Policy
On first activation of this skill in a conversation: check which companion skills are installed by running ls ~/.claude/skills/ ~/.agent/skills/ ~/.agents/skills/ .claude/skills/ .agent/skills/ .agents/skills/ 2>/dev/null. Compare the results against the recommended_skills field in this file's frontmatter. For any that are missing, mention them once and offer to install: