| name | security |
| description | Write secure web pages and applications. Use when handling user input, forms, external resources, authentication, or implementing security headers and CSP. |
| allowed-tools | Read, Write, Edit |
Security Skill
This skill ensures web pages and applications follow security best practices to prevent common vulnerabilities.
OWASP Top 10 Awareness
Key vulnerabilities this skill helps prevent:
| Vulnerability | Prevention |
|---|---|
| Injection (XSS, SQL) | Input validation, output encoding, CSP |
| Broken Authentication | Secure forms, HTTPS, secure cookies |
| Sensitive Data Exposure | HTTPS, secure headers, no secrets in HTML |
| Security Misconfiguration | Proper headers, CSP, secure defaults |
| Cross-Site Scripting (XSS) | CSP, output encoding, input validation |
| Insecure Deserialization | Validate all input, avoid eval() |
| Using Vulnerable Components | SRI for external resources |
| Insufficient Logging | Error handling without exposure |
HTTPS and Transport Security
Require HTTPS
All production sites MUST use HTTPS:
<head>
<!-- Upgrade insecure requests -->
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"/>
<!-- Only use HTTPS URLs -->
<link rel="stylesheet" href="https://example.com/styles.css"/>
<script src="https://example.com/script.js"></script>
</head>
Avoid Mixed Content
Never load HTTP resources on HTTPS pages:
<!-- BAD: Mixed content (blocked by browsers) -->
<img src="http://example.com/image.jpg"/>
<script src="http://cdn.example.com/lib.js"></script>
<!-- GOOD: Always use HTTPS -->
<img src="https://example.com/image.jpg" alt="Description"/>
<script src="https://cdn.example.com/lib.js"></script>
<!-- GOOD: Protocol-relative (inherits page protocol) -->
<img src="//example.com/image.jpg" alt="Description"/>
Security Headers
Essential Headers (Server Configuration)
Document required headers in HTML comments for server configuration:
<!--
Required Security Headers (configure on server):
# Prevent clickjacking
X-Frame-Options: DENY
# Prevent MIME sniffing
X-Content-Type-Options: nosniff
# Enable XSS filter (legacy browsers)
X-XSS-Protection: 1; mode=block
# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin
# Enforce HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains
# Permissions policy
Permissions-Policy: geolocation=(), camera=(), microphone=()
-->
Meta Tag Equivalents
Some headers can be set via meta tags:
<head>
<!-- Referrer policy -->
<meta name="referrer" content="strict-origin-when-cross-origin"/>
<!-- Content Security Policy (limited) -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'"/>
<!-- Prevent caching of sensitive pages -->
<meta http-equiv="Cache-Control" content="no-store"/>
<meta http-equiv="Pragma" content="no-cache"/>
</head>
Content Security Policy (CSP)
Basic CSP
Start with a restrictive policy:
<head>
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
"/>
</head>
CSP Directives Reference
| Directive | Purpose | Example |
|---|---|---|
default-src |
Fallback for all resource types | 'self' |
script-src |
JavaScript sources | 'self' https://cdn.example.com |
style-src |
CSS sources | 'self' 'unsafe-inline' |
img-src |
Image sources | 'self' data: https: |
font-src |
Font sources | 'self' https://fonts.gstatic.com |
connect-src |
XHR, WebSocket, fetch | 'self' https://api.example.com |
frame-src |
iframe sources | 'none' |
frame-ancestors |
Who can embed this page | 'none' |
form-action |
Form submission targets | 'self' |
base-uri |
Restrict <base> element |
'self' |
object-src |
Plugins (Flash, etc.) | 'none' |
CSP Source Values
| Value | Meaning |
|---|---|
'self' |
Same origin only |
'none' |
Block all |
'unsafe-inline' |
Allow inline (avoid if possible) |
'unsafe-eval' |
Allow eval() (avoid) |
'nonce-{random}' |
Allow specific inline with nonce |
'sha256-{hash}' |
Allow specific inline by hash |
https: |
Any HTTPS source |
data: |
Data URIs |
blob: |
Blob URIs |
Nonce-Based Scripts
For inline scripts, use nonces (server must generate unique nonce per request):
<head>
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'nonce-abc123random'"/>
</head>
<body>
<!-- Allowed: has matching nonce -->
<script nonce="abc123random">
console.log('This runs');
</script>
<!-- Blocked: no nonce -->
<script>
console.log('This is blocked');
</script>
</body>
Hash-Based Scripts
For static inline scripts, use hashes:
<head>
<!-- Hash of: console.log('Hello'); -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'sha256-xyz123...'"/>
</head>
<body>
<script>console.log('Hello');</script>
</body>
Generate hash: echo -n "console.log('Hello');" | openssl sha256 -binary | base64
Subresource Integrity (SRI)
External Resources
Always use SRI for third-party resources:
<!-- External CSS with integrity -->
<link rel="stylesheet"
href="https://cdn.example.com/lib.css"
integrity="sha384-abc123..."
crossorigin="anonymous"/>
<!-- External JavaScript with integrity -->
<script src="https://cdn.example.com/lib.js"
integrity="sha384-xyz789..."
crossorigin="anonymous"></script>
Generating SRI Hashes
# Generate SRI hash for a file
cat file.js | openssl dgst -sha384 -binary | openssl base64 -A
# Or use online tools like srihash.org
When to Use SRI
| Resource | SRI Required? |
|---|---|
| Third-party CDN scripts | Yes |
| Third-party CDN styles | Yes |
| Self-hosted resources | Optional but recommended |
| Dynamic/frequently updated | Not practical |
Form Security
CSRF Protection
Include CSRF tokens in forms:
<form method="post" action="/submit">
<!-- CSRF token (server-generated) -->
<input type="hidden" name="_csrf" value="token-from-server"/>
<!-- Form fields -->
<input type="text" name="username" autocomplete="username"/>
<button type="submit">Submit</button>
</form>
Secure Form Attributes
<form method="post"
action="https://example.com/submit"
autocomplete="off">
<!-- Password fields -->
<input type="password"
name="password"
autocomplete="new-password"
minlength="12"
required/>
<!-- Sensitive data -->
<input type="text"
name="ssn"
autocomplete="off"
inputmode="numeric"
pattern="[0-9]{9}"/>
</form>
Form Action Security
<!-- GOOD: Explicit HTTPS action -->
<form action="https://example.com/api/submit" method="post">
<!-- BAD: Relative action could be HTTP -->
<form action="/api/submit" method="post">
<!-- BAD: HTTP action -->
<form action="http://example.com/api/submit" method="post">
Input Validation
Client-Side Validation (Defense in Depth)
Use HTML5 validation attributes:
<form-field>
<label for="email">Email</label>
<input type="email"
id="email"
name="email"
required
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
maxlength="254"
autocomplete="email"/>
<output for="email"></output>
</form-field>
<form-field>
<label for="phone">Phone</label>
<input type="tel"
id="phone"
name="phone"
pattern="[0-9]{10}"
inputmode="tel"
autocomplete="tel"/>
<output for="phone"></output>
</form-field>
<form-field>
<label for="url">Website</label>
<input type="url"
id="url"
name="url"
pattern="https://.*"
placeholder="https://example.com"/>
<output for="url"></output>
</form-field>
Validation Attributes Reference
| Attribute | Purpose |
|---|---|
required |
Field must have value |
pattern |
Regex pattern to match |
minlength |
Minimum character count |
maxlength |
Maximum character count |
min / max |
Numeric range |
type="email" |
Email format validation |
type="url" |
URL format validation |
type="tel" |
Telephone (no validation, just keyboard) |
Never Trust Client Validation
<!--
IMPORTANT: Client-side validation is for UX only!
Server MUST:
- Validate all input again
- Sanitize before storage
- Encode before output
- Use parameterized queries
-->
Server-Side Validation with JSON Schema
Use the validation skill for comprehensive server-side input validation:
import { validateBody } from './middleware/validate.js';
// Validate request body against JSON Schema
app.post('/api/users',
validateBody('entities/user.create'),
createUser
);
Key security benefits:
additionalProperties: false- Rejects unknown fields (prevents mass assignment)removeAdditional: 'all'- Strips unknown properties before processing- Strict type checking - Prevents type confusion attacks
- Consistent error format - No information leakage
See the validation skill for:
- JSON Schema authoring patterns
- AJV middleware configuration
- Error response formatting
- Schema-to-type generation
Output Encoding
Prevent XSS in Dynamic Content
When inserting user data into HTML:
<!-- BAD: Direct insertion (XSS vulnerable) -->
<div id="output"></div>
<script>
document.getElementById('output').innerHTML = userInput; // DANGEROUS!
</script>
<!-- GOOD: Use textContent for text -->
<div id="output"></div>
<script>
document.getElementById('output').textContent = userInput; // Safe
</script>
<!-- GOOD: Use data attributes for values -->
<div data-user-id="123">Content</div>
Context-Specific Encoding
| Context | Encoding Method |
|---|---|
| HTML body | HTML entity encode (<, >, &) |
| HTML attributes | Attribute encode + quote |
| JavaScript | JavaScript encode or JSON.stringify |
| URL parameters | encodeURIComponent() |
| CSS | CSS encode |
Secure Links
External Links
<!-- Add rel="noopener" for security -->
<a href="https://external-site.com"
target="_blank"
rel="noopener noreferrer">
External Link
</a>
Why rel="noopener"?
Without it, the opened page can access window.opener and potentially:
- Redirect your page to a phishing site
- Access some properties of your page
Link Security Attributes
| Attribute | Purpose |
|---|---|
rel="noopener" |
Prevent window.opener access |
rel="noreferrer" |
Don't send referrer header |
rel="nofollow" |
Don't pass SEO value (user content) |
<!-- User-generated content: use all three -->
<a href="https://user-submitted-url.com"
target="_blank"
rel="noopener noreferrer nofollow">
User Link
</a>
Clickjacking Prevention
Frame Options
<!--
Server header (preferred):
X-Frame-Options: DENY
Or via CSP:
-->
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'none'"/>
Frame-Busting (Legacy Fallback)
<head>
<style>
/* Hide page if framed (fallback) */
html { display: none; }
</style>
<script>
if (self === top) {
document.documentElement.style.display = 'block';
} else {
top.location = self.location;
}
</script>
</head>
Sensitive Data Handling
Never Expose Secrets in HTML
<!-- BAD: API keys in HTML -->
<script>
const API_KEY = 'sk-secret123'; // NEVER DO THIS!
</script>
<!-- BAD: Secrets in data attributes -->
<div data-api-key="sk-secret123">
<!-- GOOD: Use server-side proxy for API calls -->
<form action="/api/proxy" method="post">
Password Fields
<input type="password"
name="password"
autocomplete="current-password"
minlength="12"
aria-describedby="password-requirements"/>
<p id="password-requirements">
Minimum 12 characters with mixed case, numbers, and symbols.
</p>
Sensitive Form Data
<!-- Prevent autocomplete on sensitive fields -->
<input type="text"
name="credit-card"
autocomplete="cc-number"
inputmode="numeric"
pattern="[0-9]{16}"/>
<!-- Prevent browser caching of sensitive pages -->
<meta http-equiv="Cache-Control" content="no-store"/>
Cookie Security
Secure Cookie Attributes
Document required cookie attributes:
<!--
Cookie Security (set via server):
Set-Cookie: session=abc123;
Secure; # HTTPS only
HttpOnly; # No JavaScript access
SameSite=Strict; # CSRF protection
Path=/; # Scope to root
Max-Age=3600; # 1 hour expiry
Authentication cookies MUST have:
- Secure (HTTPS only)
- HttpOnly (prevent XSS theft)
- SameSite=Strict or Lax (CSRF protection)
-->
SameSite Values
| Value | Behavior |
|---|---|
Strict |
Cookie only sent for same-site requests |
Lax |
Sent for same-site + top-level navigation |
None |
Sent for all requests (requires Secure) |
JavaScript Security
Safe DOM Manipulation
// BAD: innerHTML with user data
element.innerHTML = userInput;
// GOOD: textContent for text
element.textContent = userInput;
// GOOD: createElement for structure
const div = document.createElement('div');
div.textContent = userInput;
parent.appendChild(div);
// GOOD: Template with encoded values
const template = document.getElementById('item-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.name').textContent = userName;
Avoid Dangerous Functions
// NEVER USE with user input:
eval(userInput); // Code execution
new Function(userInput); // Code execution
setTimeout(userInput, 1000); // If string, same as eval
setInterval(userInput, 1000); // If string, same as eval
element.innerHTML = userInput; // XSS
document.write(userInput); // XSS
location.href = userInput; // Open redirect
URL Validation
// Validate URLs before use
function isValidHttpUrl(string) {
try {
const url = new URL(string);
return url.protocol === 'https:' || url.protocol === 'http:';
} catch {
return false;
}
}
// Prevent javascript: URLs
function isSafeUrl(string) {
try {
const url = new URL(string, window.location.origin);
return !url.protocol.startsWith('javascript');
} catch {
return false;
}
}
Error Handling
Don't Expose Stack Traces
<!-- BAD: Detailed error in HTML -->
<div class="error">
Error: Cannot read property 'id' of undefined
at processUser (app.js:123)
at handleSubmit (app.js:456)
</div>
<!-- GOOD: Generic user-friendly message -->
<div class="error" role="alert">
<p>Something went wrong. Please try again.</p>
<p>If the problem persists, contact support.</p>
</div>
Error Logging
// Log details server-side, show generic message to user
try {
await submitForm(data);
} catch (error) {
// Send to logging service (not console in production)
logError({ error, context: 'form-submit', timestamp: Date.now() });
// Show generic message to user
showError('Unable to submit form. Please try again.');
}
Security Checklist
Before deploying:
Transport
- Site uses HTTPS exclusively
- No mixed content (HTTP resources on HTTPS page)
- HSTS header configured (server-side)
Headers
- Content-Security-Policy defined
- X-Frame-Options or frame-ancestors set
- X-Content-Type-Options: nosniff
- Referrer-Policy configured
Resources
- External scripts have SRI integrity attributes
- External styles have SRI integrity attributes
- No inline scripts (or use nonces/hashes)
Forms
- CSRF tokens included
- Form actions use HTTPS
- Input validation attributes set
- Autocomplete appropriate for field type
Links
- External links have
rel="noopener noreferrer" - User-generated links have
rel="nofollow"too - No
javascript:URLs
Data
- No secrets in HTML source
- No sensitive data in URLs
- Appropriate cache headers for sensitive pages
- Error messages don't expose system details
JavaScript
- No eval() or equivalent with user input
- textContent used instead of innerHTML for user data
- URLs validated before use
Related Skills
| Skill | Security Overlap |
|---|---|
validation |
Server-side input validation with JSON Schema |
forms |
Client-side form validation, autocomplete |
javascript-author |
Safe DOM manipulation |
metadata |
Security meta tags |
performance |
Resource loading (SRI compatible) |
Common Mistakes
| Mistake | Risk | Solution |
|---|---|---|
| HTTP resources | Mixed content blocked | Use HTTPS everywhere |
| No SRI on CDN scripts | Supply chain attack | Add integrity attribute |
| innerHTML with user data | XSS | Use textContent |
| Missing CSRF token | CSRF attacks | Include token in forms |
| target="_blank" without rel | Tab nabbing | Add rel="noopener" |
| Secrets in HTML | Credential theft | Use server-side proxy |
| Detailed error messages | Information disclosure | Generic messages |
| No CSP | XSS easier to exploit | Implement CSP |