
Owasp Security Check
- 1.3k installs
- 93 repo stars
- Updated February 1, 2026
- sergiodxa/agent-skills
owasp-security-check is an agent skill auditing web apps and REST APIs against OWASP Top 10 with prioritized rules.
About
The owasp-security-check skill provides comprehensive security audit patterns for web applications and REST APIs with twenty rules across five categories covering OWASP Top 10 and common web vulnerabilities. Apply when auditing codebases, reviewing auth implementations, evaluating API security, assessing data protection, checking deployment configuration, or before production deploys. Workflow identifies application type as web app, REST API, SPA, SSR, or mixed, scans CRITICAL rules first then HIGH and MEDIUM, loads specific rules from the rules directory, and reports severity with file location, impact, and remediation code examples. CRITICAL categories include Authentication and Authorization with broken-access-control and authentication-failures, Data Protection with cryptographic-failures and sensitive-data-exposure, and Input/Output Security. HIGH covers Configuration and Headers; MEDIUM covers API and Monitoring. Example fixes show IDOR prevention with session ownership checks, bcrypt instead of MD5 password hashing, and strong password validation requiring twelve characters with mixed character classes. Findings format lists Severity, Category, File, Issue, Impact, and Fix.
- Twenty rules across five categories aligned with OWASP Top 10 patterns.
- Audit priority: CRITICAL auth and data rules, then HIGH config, then MEDIUM API.
- Loads detailed guidance from @rules/ files per vulnerability category.
- Reports include severity, file path, impact, and remediation code examples.
- Covers broken access control, weak crypto, sensitive exposure, and input security.
Owasp Security Check by the numbers
- 1,329 all-time installs (skills.sh)
- +38 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #341 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
owasp-security-check capabilities & compatibility
- Capabilities
- prioritized rule scanning · per category rule file loading · structured finding reports · remediation code examples · multi app type support
- Use cases
- refactoring
What owasp-security-check says it does
Comprehensive security audit patterns for web applications and REST APIs.
Start with CRITICAL rules, then HIGH, then MEDIUM
npx skills add https://github.com/sergiodxa/agent-skills --skill owasp-security-checkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 93 |
| Security audit | 3 / 3 scanners passed |
| Last updated | February 1, 2026 |
| Repository | sergiodxa/agent-skills ↗ |
How do I systematically review this codebase for OWASP security vulnerabilities before launch?
Audit web apps and REST APIs for OWASP Top 10 vulnerabilities with prioritized rule-based review.
Who is it for?
Teams auditing web apps and APIs for auth, crypto, and injection vulnerabilities.
Skip if: Skip for non-web infrastructure like raw ML training without HTTP surfaces.
When should I use this skill?
User audits security, reviews auth, checks OWASP, or prepares production deployment.
What you get
Severity-ranked findings with file locations, impacts, and concrete remediation snippets.
- endpoint security checklist results
- flagged mass assignment and pagination issues
By the numbers
- Tagged medium impact with api, rest, mass-assignment, and versioning labels
- Checklist covers four core risk areas: mass assignment, over-fetching, resource exhaustion, and API abuse
Files
OWASP Security Check
Comprehensive security audit patterns for web applications and REST APIs. Contains 20 rules across 5 categories covering OWASP Top 10 and common web vulnerabilities.
When to Apply
Use this skill when:
- Auditing a codebase for security vulnerabilities
- Reviewing user-provided file or folder for security issues
- Checking authentication/authorization implementations
- Evaluating REST API security
- Assessing data protection measures
- Reviewing configuration and deployment settings
- Before production deployment
- After adding new features that handle sensitive data
How to Use This Skill
1. Identify application type - Web app, REST API, SPA, SSR, or mixed 2. Scan by priority - Start with CRITICAL rules, then HIGH, then MEDIUM 3. Review relevant rule files - Load specific rules from @rules/ directory 4. Report findings - Note severity, file location, and impact 5. Provide remediation - Give concrete code examples for fixes
Audit Workflow
Step 1: Systematic Review by Priority
Work through categories by priority:
1. CRITICAL: Authentication & Authorization, Data Protection, Input/Output Security 2. HIGH: Configuration & Headers 3. MEDIUM: API & Monitoring
Step 2: Generate Report
Format findings as:
- Severity: CRITICAL | HIGH | MEDIUM | LOW
- Category: Rule name
- File: Path and line number
- Issue: What's wrong
- Impact: Security consequence
- Fix: Code example of remediation
Rules Summary
Authentication & Authorization (CRITICAL)
broken-access-control - @rules/broken-access-control.md
Check for missing authorization, IDOR, privilege escalation.
// Bad: No authorization check
async function getUser(req: Request): Promise<Response> {
let url = new URL(req.url);
let userId = url.searchParams.get("id");
let user = await db.user.findUnique({ where: { id: userId } });
return new Response(JSON.stringify(user));
}
// Good: Verify ownership
async function getUser(req: Request): Promise<Response> {
let session = await getSession(req);
let url = new URL(req.url);
let userId = url.searchParams.get("id");
if (session.userId !== userId && !session.isAdmin) {
return new Response("Forbidden", { status: 403 });
}
let user = await db.user.findUnique({ where: { id: userId } });
return new Response(JSON.stringify(user));
}authentication-failures - @rules/authentication-failures.md
Check for weak authentication, missing MFA, session issues.
// Bad: Weak password check
if (password.length >= 6) {
/* allow */
}
// Good: Strong password requirements
function validatePassword(password: string) {
if (password.length < 12) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/[a-z]/.test(password)) return false;
if (!/[0-9]/.test(password)) return false;
if (!/[^A-Za-z0-9]/.test(password)) return false;
return true;
}Data Protection (CRITICAL)
cryptographic-failures - @rules/cryptographic-failures.md
Check for weak encryption, plaintext storage, bad hashing.
// Bad: MD5 for passwords
let hash = crypto.createHash("md5").update(password).digest("hex");
// Good: bcrypt with salt
let hash = await bcrypt(password, 12);sensitive-data-exposure - @rules/sensitive-data-exposure.md
Check for PII in logs/responses, error messages leaking info.
// Bad: Exposing sensitive data
return new Response(JSON.stringify(user)); // Contains password hash, email, etc.
// Good: Return only needed fields
return new Response(
JSON.stringify({
id: user.id,
username: user.username,
displayName: user.displayName,
}),
);data-integrity-failures - @rules/data-integrity-failures.md
Check for unsigned data, insecure deserialization.
// Bad: Trusting unsigned JWT
let decoded = JSON.parse(atob(token.split(".")[1]));
if (decoded.isAdmin) {
/* grant access */
}
// Good: Verify signature
let payload = await verifyJWT(token, secret);secrets-management - @rules/secrets-management.md
Check for hardcoded secrets, exposed env vars.
// Bad: Hardcoded secret
const API_KEY = "sk_live_a1b2c3d4e5f6";
// Good: Environment variables
let API_KEY = process.env.API_KEY;
if (!API_KEY) throw new Error("API_KEY not configured");Input/Output Security (CRITICAL)
injection-attacks - @rules/injection-attacks.md
Check for SQL, XSS, NoSQL, Command, Path Traversal injection.
// Bad: SQL injection
let query = `SELECT * FROM users WHERE email = '${email}'`;
// Good: Parameterized query
let user = await db.user.findUnique({ where: { email } });ssrf-attacks - @rules/ssrf-attacks.md
Check for unvalidated URLs, internal network access.
// Bad: Fetching user-provided URL
let url = await req.json().then((d) => d.url);
let response = await fetch(url);
// Good: Validate against allowlist
const ALLOWED_DOMAINS = ["api.example.com", "cdn.example.com"];
let url = new URL(await req.json().then((d) => d.url));
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
return new Response("Invalid URL", { status: 400 });
}file-upload-security - @rules/file-upload-security.md
Check for unrestricted uploads, MIME validation.
// Bad: No file type validation
let file = await req.formData().then((fd) => fd.get("file"));
await writeFile(`./uploads/${file.name}`, file);
// Good: Validate type and extension
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
const ALLOWED_EXTS = [".jpg", ".jpeg", ".png", ".webp"];
let file = await req.formData().then((fd) => fd.get("file") as File);
if (!ALLOWED_TYPES.includes(file.type)) {
return new Response("Invalid file type", { status: 400 });
}redirect-validation - @rules/redirect-validation.md
Check for open redirects, unvalidated redirect URLs.
// Bad: Unvalidated redirect
let returnUrl = new URL(req.url).searchParams.get("return");
return Response.redirect(returnUrl);
// Good: Validate redirect URL
let returnUrl = new URL(req.url).searchParams.get("return");
let allowed = ["/dashboard", "/profile", "/settings"];
if (!allowed.includes(returnUrl)) {
return Response.redirect("/");
}Configuration & Headers (HIGH)
insecure-design - @rules/insecure-design.md
Check for security anti-patterns in architecture.
// Bad: Security by obscurity
let isAdmin = req.headers.get("x-admin-secret") === "admin123";
// Good: Proper role-based access control
let session = await getSession(req);
let isAdmin = await db.user
.findUnique({
where: { id: session.userId },
})
.then((u) => u.role === "ADMIN");security-misconfiguration - @rules/security-misconfiguration.md
Check for default configs, debug mode, error handling.
// Bad: Exposing stack traces
catch (error) {
return new Response(error.stack, { status: 500 });
}
// Good: Generic error message
catch (error) {
console.error(error); // Log server-side only
return new Response("Internal server error", { status: 500 });
}security-headers - @rules/security-headers.md
Check for CSP, HSTS, X-Frame-Options, etc.
// Bad: No security headers
return new Response(html);
// Good: Security headers set
return new Response(html, {
headers: {
"Content-Security-Policy": "default-src 'self'",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
},
});cors-configuration - @rules/cors-configuration.md
Check for overly permissive CORS.
// Bad: Wildcard with credentials
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Credentials", "true");
// Good: Specific origin
let allowedOrigins = ["https://app.example.com"];
let origin = req.headers.get("origin");
if (origin && allowedOrigins.includes(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
}csrf-protection - @rules/csrf-protection.md
Check for CSRF tokens, SameSite cookies.
// Bad: No CSRF protection
let cookies = parseCookies(req.headers.get("cookie"));
let session = await getSession(cookies.sessionId);
// Good: SameSite cookie + token validation
return new Response("OK", {
headers: {
"Set-Cookie": "session=abc; SameSite=Strict; Secure; HttpOnly",
},
});session-security - @rules/session-security.md
Check for cookie flags, JWT issues, token storage.
// Bad: Insecure cookie
return new Response("OK", {
headers: { "Set-Cookie": "session=abc123" },
});
// Good: Secure cookie with all flags
return new Response("OK", {
headers: {
"Set-Cookie":
"session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600",
},
});API & Monitoring (MEDIUM-HIGH)
api-security - @rules/api-security.md
Check for REST API vulnerabilities, mass assignment.
// Bad: Mass assignment vulnerability
let userData = await req.json();
await db.user.update({ where: { id }, data: userData });
// Good: Explicitly allow fields
let { displayName, bio } = await req.json();
await db.user.update({
where: { id },
data: { displayName, bio }, // Only allowed fields
});rate-limiting - @rules/rate-limiting.md
Check for missing rate limits, brute force prevention.
// Bad: No rate limiting
async function login(req: Request): Promise<Response> {
let { email, password } = await req.json();
// Allows unlimited login attempts
}
// Good: Rate limiting
let ip = req.headers.get("x-forwarded-for");
let { success } = await ratelimit.limit(ip);
if (!success) {
return new Response("Too many requests", { status: 429 });
}logging-monitoring - @rules/logging-monitoring.md
Check for insufficient logging, sensitive data in logs.
// Bad: Logging sensitive data
console.log("User login:", { email, password, ssn });
// Good: Log events without sensitive data
console.log("User login attempt", {
email,
ip: req.headers.get("x-forwarded-for"),
timestamp: new Date().toISOString(),
});vulnerable-dependencies - @rules/vulnerable-dependencies.md
Check for outdated packages, known CVEs.
# Bad: No dependency checking
npm install
# Good: Regular audits
npm audit
npm audit fixCommon Vulnerability Patterns
Quick reference of patterns to look for:
- User input without validation:
req.json()→ immediate use - Missing auth checks: Routes without authorization middleware
- Hardcoded secrets: Strings containing "password", "secret", "key"
- SQL injection: String concatenation in queries
- XSS:
dangerouslySetInnerHTML,.innerHTML - Weak crypto:
md5,sha1for passwords - Missing headers: No CSP, HSTS, or security headers
- CORS wildcards:
Access-Control-Allow-Origin: *with credentials - Insecure cookies: Missing Secure, HttpOnly, SameSite flags
- Path traversal: User input in file paths without validation
Severity Quick Reference
Fix Immediately (CRITICAL):
- SQL/XSS/Command Injection
- Missing authentication on sensitive endpoints
- Hardcoded secrets in code
- Plaintext password storage
- IDOR vulnerabilities
Fix Soon (HIGH):
- Missing CSRF protection
- Weak password requirements
- Missing security headers
- Overly permissive CORS
- Insecure session management
Fix When Possible (MEDIUM):
- Missing rate limiting
- Incomplete logging
- Outdated dependencies (no known exploits)
- Missing input validation on non-critical fields
Improve (LOW):
- Missing optional security headers
- Verbose error messages (non-production)
- Suboptimal crypto parameters
REST API Security
Check for REST API vulnerabilities including mass assignment, lack of validation, and missing resource limits.
Related: Input validation in injection-attacks.md. Authentication in authentication-failures.md. Rate limiting in rate-limiting.md.
Why
- Mass assignment: Users modify protected fields
- Over-fetching: Expose unnecessary data
- Resource exhaustion: Unlimited result sets
- API abuse: Missing versioning and documentation
What to Check
- [ ] Mass assignment in update operations
- [ ] No pagination on list endpoints
- [ ] Missing Content-Type validation
- [ ] No API versioning
- [ ] Excessive data in responses
- [ ] Missing rate limits
Bad Patterns
// Bad: Mass assignment
async function updateUser(req: Request): Promise<Response> {
let session = await getSession(req);
let data = await req.json();
// VULNERABLE: User can set isAdmin, role, etc.!
await db.users.update({
where: { id: session.userId },
data, // Dangerous - accepts all fields!
});
return new Response("Updated");
}
// Bad: No pagination
async function getUsers(req: Request): Promise<Response> {
// VULNERABLE: Could return millions of records
let users = await db.users.findMany();
return Response.json(users);
}
// Bad: No input validation
async function createPost(req: Request): Promise<Response> {
let data = await req.json();
// VULNERABLE: No validation of data types or values
await db.posts.create({ data });
return new Response("Created", { status: 201 });
}Good Patterns
// Good: Explicit field allowlist
async function updateUser(req: Request): Promise<Response> {
let session = await getSession(req);
let body = await req.json();
let allowedFields = {
displayName: body.displayName,
bio: body.bio,
avatar: body.avatar,
};
if (
allowedFields.displayName &&
typeof allowedFields.displayName !== "string"
) {
return new Response("Invalid displayName", { status: 400 });
}
await db.users.update({
where: { id: session.userId },
data: allowedFields,
});
return new Response("Updated");
}
// Good: Pagination with limits
async function getUsers(req: Request): Promise<Response> {
let url = new URL(req.url);
let page = parseInt(url.searchParams.get("page") || "1");
let limit = Math.min(parseInt(url.searchParams.get("limit") || "20"), 100);
let users = await db.users.findMany({
take: limit,
skip: (page - 1) * limit,
});
return Response.json({ data: users, page, limit });
}
// Good: Input validation
async function createPost(req: Request): Promise<Response> {
let session = await getSession(req);
let body = await req.json();
if (
!body.title ||
typeof body.title !== "string" ||
body.title.length > 200
) {
return new Response("Invalid title", { status: 400 });
}
if (
!body.content ||
typeof body.content !== "string" ||
body.content.length > 50000
) {
return new Response("Invalid content", { status: 400 });
}
await db.posts.create({
data: {
title: body.title,
content: body.content,
authorId: session.userId,
},
});
return new Response("Created", { status: 201 });
}Rules
1. Prevent mass assignment - Explicitly define allowed fields 2. Always paginate lists - Enforce maximum page size 3. Validate input types - Check types and constraints 4. Version your API - Use /api/v1/ prefix for versioning 5. Limit response data - Return only necessary fields 6. Validate Content-Type - Ensure correct headers
Authentication Failures
Check for weak authentication mechanisms, missing MFA, session management issues, and credential handling vulnerabilities.
Related: Session security in session-security.md. Rate limiting in rate-limiting.md.
Why
- Account takeover: Attackers gain unauthorized access to user accounts
- Credential stuffing: Weak auth enables automated attacks
- Session hijacking: Improper session management allows theft
- Brute force attacks: Weak passwords and no rate limiting enable guessing
What to Check
- [ ] Weak password requirements (length < 12, no complexity)
- [ ] No multi-factor authentication option
- [ ] Passwords stored in plaintext or with weak hashing (MD5, SHA1)
- [ ] Missing account lockout after failed attempts
- [ ] Session tokens predictable or not securely generated
- [ ] No session expiration or timeout
- [ ] Session not regenerated after login
- [ ] Credentials exposed in URLs or logs
Bad Patterns
// Bad: Weak password hashing (SHA-256 too fast)
const hash = crypto.createHash("sha256").update(password).digest("hex");
// Bad: No password requirements
async function signup(req: Request): Promise<Response> {
let { email, password } = await req.json();
// Accepts "123" as valid password!
await db.users.create({
data: { email, password: await bcrypt(password, 10) },
});
}
// Bad: Timing attack reveals if email exists
const user = await db.users.findUnique({ where: { email } });
if (!user) return new Response("Invalid", { status: 401 }); // Early return!
if (!(await bcrypt.compare(password, user.password))) {
return new Response("Invalid", { status: 401 });
}
// Bad: No rate limiting or account lockout
async function login(req: Request): Promise<Response> {
// Unlimited attempts allowed!
let user = await authenticate(email, password);
}Good Patterns
// Good: bcrypt with proper cost factor
const hash = await bcrypt(password, 12); // Cost factor 12+
// Good: Strong password validation
function validatePassword(password: string): string | null {
if (password.length < 12) return "Password must be ≥12 characters";
if (!/[A-Z]/.test(password)) return "Must include uppercase";
if (!/[a-z]/.test(password)) return "Must include lowercase";
if (!/[0-9]/.test(password)) return "Must include number";
return null;
}
async function signup(req: Request): Promise<Response> {
let { email, password } = await req.json();
let error = validatePassword(password);
if (error) return new Response(error, { status: 400 });
await db.users.create({
data: { email, password: await bcrypt(password, 12) },
});
}
// Good: Constant-time comparison
async function login(req: Request): Promise<Response> {
let { email, password } = await req.json();
let user = await db.users.findUnique({ where: { email } });
// Always compare (constant time)
let hash = user?.password || "$2b$12$fakehash...";
let valid = await bcrypt.compare(password, hash);
if (!user || !valid) {
return new Response("Invalid credentials", { status: 401 });
}
return createSession(user);
}
// Good: Account lockout after failed attempts
async function loginWithLockout(req: Request): Promise<Response> {
let { email, password } = await req.json();
let user = await db.users.findUnique({ where: { email } });
if (user?.lockedUntil && user.lockedUntil > new Date()) {
return new Response("Account locked", { status: 423 });
}
let valid = user && (await bcrypt.compare(password, user.password));
if (!user || !valid) {
let attempts = (user?.failedAttempts || 0) + 1;
await db.users.update({
where: { email },
data: {
failedAttempts: attempts,
lockedUntil:
attempts >= 5 ? new Date(Date.now() + 30 * 60 * 1000) : null,
},
});
return new Response("Invalid credentials", { status: 401 });
}
// Reset on success
await db.users.update({
where: { id: user.id },
data: { failedAttempts: 0, lockedUntil: null },
});
return createSession(user);
}Rules
1. Require strong passwords - Minimum 12 characters with complexity 2. Hash passwords properly - Use bcrypt, argon2, or scrypt (never MD5/SHA1) 3. Implement rate limiting - Limit authentication attempts per IP/account 4. Use secure session tokens - Cryptographically random tokens 5. Set session expiration - Both absolute and idle timeout 6. Regenerate session on login - Prevent session fixation attacks 7. Implement account lockout - Temporarily lock after multiple failures 8. Support MFA - Especially for privileged accounts 9. Never log credentials - Don't log passwords, tokens, or reset links
Broken Access Control
Check for missing authorization checks, insecure direct object references (IDOR), privilege escalation, and path traversal.
Related: Path traversal in injection-attacks.md and file-upload-security.md.
Why
- Data breach: Users access others' sensitive data
- Privilege escalation: Regular users gain admin access
- Data manipulation: Unauthorized modification or deletion
- Compliance violation: GDPR, HIPAA, PCI-DSS penalties
What to Check
- [ ] Routes accessing resources without verifying ownership
- [ ] User IDs taken from request params without validation
- [ ] Admin endpoints without role checks
- [ ] File paths constructed from user input
- [ ] Authorization checks that can be bypassed
- [ ] Horizontal privilege escalation (user A→user B's data)
- [ ] Vertical privilege escalation (user→admin functions)
Bad Patterns
// Bad: No authorization check
const userId = url.searchParams.get("id");
const user = await db.users.findUnique({ where: { id: userId } });
return Response.json(user); // Anyone can access!
// Bad: No role check
await db.users.delete({ where: { id: userId } }); // No admin verification!
// Bad: Path traversal
const filename = url.searchParams.get("file");
const content = await fs.readFile(`./uploads/${filename}`, "utf-8");Good Patterns
// Good: Verify ownership before access
async function getUserProfile(req: Request): Promise<Response> {
let session = await getSession(req);
let url = new URL(req.url);
let userId = url.searchParams.get("id");
if (session.userId !== userId && !session.isAdmin) {
return new Response("Forbidden", { status: 403 });
}
let user = await db.users.findUnique({ where: { id: userId } });
return Response.json(user);
}
// Good: Role-based access control
async function deleteUser(req: Request): Promise<Response> {
let session = await getSession(req);
let user = await db.users.findUnique({
where: { id: session.userId },
select: { role: true },
});
if (user.role !== "ADMIN") {
return new Response("Forbidden", { status: 403 });
}
let url = new URL(req.url);
let userId = url.searchParams.get("id");
await db.users.delete({ where: { id: userId } });
return new Response("Deleted");
}
// Good: Prevent path traversal
async function downloadFile(req: Request): Promise<Response> {
let url = new URL(req.url);
let filename = url.searchParams.get("file");
let ALLOWED = ["terms.pdf", "privacy.pdf", "guide.pdf"];
if (
!filename ||
!ALLOWED.includes(filename) ||
filename.includes("..") ||
filename.includes("/")
) {
return new Response("Invalid file", { status: 400 });
}
let content = await fs.readFile(`./documents/${filename}`, "utf-8");
return new Response(content);
}Rules
1. Never trust user input for authorization - Verify against server-side session 2. Check ownership on every resource access - Don't assume URL ID is valid 3. Implement deny-by-default - Require explicit permission grants 4. Use role-based access control - Define clear roles and check them 5. Validate file paths - Never construct paths directly from user input 6. Log authorization failures - Track denied access for monitoring 7. Test with different roles - Verify unprivileged users can't access privileged resources
CORS Configuration
Check for overly permissive Cross-Origin Resource Sharing (CORS) policies that allow unauthorized cross-origin requests.
Related: CSRF protection in csrf-protection.md. Security headers in security-headers.md.
Why
- Unauthorized access: Malicious sites can access your API
- Credential theft: CORS with credentials exposes sensitive data
- CSRF attacks: Improper CORS enables cross-site attacks
- Data leakage: Private APIs exposed to untrusted origins
What to Check
- [ ]
Access-Control-Allow-Origin: *with credentials - [ ] Reflecting request origin without validation
- [ ] Missing origin validation
- [ ] Overly permissive allowed methods/headers
- [ ] No CORS policy on sensitive endpoints
Bad Patterns
// Bad: Wildcard with credentials
return Response.json(data, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
},
});
// Bad: Reflecting any origin
const origin = req.headers.get("origin");
return Response.json(data, {
headers: {
"Access-Control-Allow-Origin": origin || "*",
"Access-Control-Allow-Credentials": "true",
},
});
// Bad: Weak regex
return /.*\.yourdomain\.com/.test(origin); // evil-yourdomain.com matches!Good Patterns
// Good: Strict origin allowlist
const ALLOWED_ORIGINS = [
"https://yourdomain.com",
"https://app.yourdomain.com",
"https://admin.yourdomain.com",
];
async function handler(req: Request): Promise<Response> {
let origin = req.headers.get("origin");
let corsHeaders: Record<string, string> = {};
if (origin && ALLOWED_ORIGINS.includes(origin)) {
corsHeaders["Access-Control-Allow-Origin"] = origin;
corsHeaders["Access-Control-Allow-Credentials"] = "true";
corsHeaders["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE";
corsHeaders["Access-Control-Allow-Headers"] = "Content-Type, Authorization";
}
return Response.json(data, { headers: corsHeaders });
}
// Good: Environment-based CORS
function getAllowedOrigins(): string[] {
if (process.env.NODE_ENV === "production") {
return ["https://yourdomain.com", "https://app.yourdomain.com"];
}
return ["http://localhost:3000", "http://localhost:5173"];
}
// Good: Preflight request handling
async function corsHandler(req: Request): Response | null {
let origin = req.headers.get("origin");
let allowed = getAllowedOrigins();
if (!origin || !allowed.includes(origin)) {
return new Response("Origin not allowed", { status: 403 });
}
let corsHeaders = {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
};
if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders });
}
return null;
}Rules
1. *Never use `Access-Control-Allow-Origin: ` with credentials - Pick one or the other 2. Use strict origin allowlist - Explicitly list allowed origins 3. Validate origin before reflecting - Don't blindly reflect request origin 4. Separate dev and prod origins - Don't allow localhost in production 5. Limit allowed methods - Only necessary HTTP methods 6. Limit allowed headers - Only required headers 7. Handle preflight requests** - Respond to OPTIONS correctly
Cryptographic Failures
Check for weak encryption, improper key management, plaintext storage of sensitive data, and missing encryption in transit.
Related: Password hashing in authentication-failures.md. Secrets in secrets-management.md. Data signing in data-integrity-failures.md.
Why
- Data breach: Sensitive data exposed if stolen
- Compliance violation: GDPR, PCI-DSS require encryption
- Man-in-the-middle: Unencrypted connections intercepted
- Password compromise: Weak hashing enables rainbow table attacks
What to Check
- [ ] Sensitive data stored in plaintext (passwords, tokens, PII)
- [ ] Weak hashing algorithms (MD5, SHA1) for passwords
- [ ] Weak encryption algorithms (DES, RC4, ECB mode)
- [ ] Hardcoded encryption keys or predictable keys
- [ ] Missing HTTPS/TLS for data transmission
- [ ] Insufficient key length (< 2048 bits for RSA, < 256 bits symmetric)
- [ ] No encryption for sensitive data at rest
Bad Patterns
// Bad: MD5 for password hashing
async function hashPassword(password: string): Promise<string> {
// VULNERABLE: MD5 is too fast, easily cracked
return crypto.createHash("md5").update(password).digest("hex");
}
// Bad: Storing passwords in plaintext
await db.users.create({
data: {
email,
password, // VULNERABLE: Plaintext!
},
});
// Bad: Weak encryption algorithm
const cipher = crypto.createCipher("des", "weak-key"); // VULNERABLE: DES is weak
// Bad: Hardcoded encryption key
const ENCRYPTION_KEY = "my-secret-key-12345"; // VULNERABLE: Hardcoded
function encryptData(data: string): string {
const cipher = crypto.createCipheriv("aes-256-cbc", ENCRYPTION_KEY, iv);
return cipher.update(data, "utf8", "hex");
}
// Bad: No encryption for sensitive data
await db.creditCards.create({
data: {
number: "4111111111111111", // VULNERABLE: Plaintext
cvv: "123",
expiresAt: "12/25",
},
});Good Patterns
// Good: bcrypt for password hashing
async function hashPassword(password: string): Promise<string> {
return await bcrypt(password, 12);
}
// Good: AES-256-GCM encryption
function encryptData(plaintext: string): { encrypted: string; iv: string } {
let key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
let iv = crypto.randomBytes(16);
let cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex");
encrypted += cipher.getAuthTag().toString("hex");
return { encrypted, iv: iv.toString("hex") };
}
function decryptData(encrypted: string, ivHex: string): string {
let key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
let iv = Buffer.from(ivHex, "hex");
let authTag = Buffer.from(encrypted.slice(-32), "hex");
let ciphertext = encrypted.slice(0, -32);
let decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(authTag);
return decipher.update(ciphertext, "hex", "utf8") + decipher.final("utf8");
}
// Good: Encrypt sensitive fields
async function saveCreditCard(req: Request): Promise<Response> {
let { number, cvv } = await req.json();
let { encrypted: encryptedNumber, iv: numberIv } = encryptData(number);
let { encrypted: encryptedCvv, iv: cvvIv } = encryptData(cvv);
await db.creditCards.create({
data: { encryptedNumber, numberIv, encryptedCvv, cvvIv },
});
return new Response("Saved", { status: 201 });
}Rules
1. Use strong password hashing - bcrypt, argon2, or scrypt (never MD5/SHA1) 2. Use modern encryption - AES-256-GCM or ChaCha20-Poly1305 3. Never hardcode keys - Use environment variables or key management systems 4. Encrypt sensitive data at rest - PII, credentials, financial data 5. Enforce HTTPS/TLS - All data in transit must be encrypted 6. Use sufficient key lengths - RSA ≥ 2048 bits, symmetric ≥ 256 bits 7. Generate random IVs - New random IV for each encryption operation 8. Rotate keys regularly - Implement key rotation policies
CSRF Protection
Check for Cross-Site Request Forgery protection on state-changing operations.
Related: Session cookie configuration is covered in session-security.md. CORS configuration is covered in cors-configuration.md.
Why
- Unauthorized actions: Attackers perform actions as victim
- Account takeover: Change email/password without consent
- Financial fraud: Unauthorized transfers
- Data manipulation: Modify user data
What to Check
Vulnerability Indicators:
- [ ] State-changing endpoints accept GET requests
- [ ] No CSRF tokens on forms
- [ ] Cookies without SameSite attribute
- [ ] Missing Origin/Referer validation
- [ ] No double-submit cookie pattern
Bad Patterns
// Bad: No SameSite on cookie
return new Response("OK", {
headers: { "Set-Cookie": "session=abc123; HttpOnly; Secure" },
});
// Bad: State change via GET
async function deleteAccount(req: Request): Promise<Response> {
let userId = new URL(req.url).searchParams.get("id");
await db.users.delete({ where: { id: userId } });
}
// Bad: No CSRF token
const { to, amount } = await req.json();
await transfer(to, amount); // Attacker can trigger!Good Patterns
// Good: SameSite cookie
async function login(req: Request): Promise<Response> {
return new Response("OK", {
headers: {
"Set-Cookie": "session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/",
},
});
}
// Good: CSRF token validation
async function generateCSRFToken(sessionId: string): Promise<string> {
let token = crypto.randomBytes(32).toString("hex");
await db.csrfToken.create({
data: {
token,
sessionId,
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
},
});
return token;
}
async function validateCSRFToken(
sessionId: string,
token: string,
): Promise<boolean> {
let stored = await db.csrfToken.findFirst({
where: { token, sessionId, expiresAt: { gt: new Date() } },
});
if (stored) {
await db.csrfToken.delete({ where: { id: stored.id } });
return true;
}
return false;
}
async function transferMoney(req: Request): Promise<Response> {
let session = await getSession(req);
let { to, amount, csrfToken } = await req.json();
if (!(await validateCSRFToken(session.id, csrfToken))) {
return new Response("Invalid CSRF token", { status: 403 });
}
await transfer(to, amount);
return new Response("OK");
}
// Good: Double-submit cookie pattern
async function setupCSRF(req: Request): Promise<Response> {
let token = crypto.randomBytes(32).toString("hex");
return Response.json(
{ csrfToken: token },
{
headers: {
"Set-Cookie": `csrf=${token}; SameSite=Strict; Secure`,
"Content-Type": "application/json",
},
},
);
}
async function validateDoubleSubmit(req: Request): Promise<boolean> {
let cookies = parseCookies(req.headers.get("cookie"));
let { csrfToken } = await req.json();
return cookies.csrf === csrfToken;
}Rules
1. Use SameSite=Strict or Lax - On all session cookies 2. No state changes via GET - Use POST/PUT/DELETE 3. Implement CSRF tokens - For session-based auth 4. Double-submit cookie - Alternative to tokens 5. Validate Origin header - Additional protection layer
Software and Data Integrity Failures
Check for unsigned data, insecure deserialization, and lack of integrity verification in code and data.
Related: JWT signing in cryptographic-failures.md and session-security.md. Dependency integrity in vulnerable-dependencies.md.
Why
- Data tampering: Attackers modify unsigned data
- Remote code execution: Insecure deserialization exploits
- Supply chain attacks: Unsigned packages or builds
- Trust violations: Cannot verify data authenticity
What to Check
- [ ] JWT tokens decoded without signature verification
- [ ] Accepting unsigned or unverified data
- [ ] Insecure deserialization of user input
- [ ] No integrity checks on file downloads
- [ ] Missing code signing in CI/CD
- [ ] Auto-update without verification
- [ ] Using eval() or Function() with external data
Bad Patterns
// Bad: No signature verification
async function handleWebhook(req: Request): Promise<Response> {
const payload = await req.json();
// Trusting payload without verification!
await processOrder(payload);
}
// Bad: JWT without verification
async function getUser(req: Request): Promise<Response> {
let token = req.headers.get("authorization")?.split(" ")[1];
let payload = JSON.parse(atob(token!.split(".")[1])); // Just decode!
// Attacker can modify payload
return Response.json({ userId: payload.sub });
}
// Bad: No integrity check on downloads
async function downloadUpdate(req: Request): Promise<Response> {
let file = await fetch("https://cdn.example.com/update.zip");
// No checksum verification
return new Response(file.body);
}Good Patterns
// Good: Verify webhook signature
async function handleWebhook(req: Request): Promise<Response> {
let signature = req.headers.get("x-webhook-signature");
let payload = await req.text();
let expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(payload)
.digest("hex");
if (signature !== expected) {
return new Response("Invalid signature", { status: 401 });
}
await processOrder(JSON.parse(payload));
return new Response("OK");
}
// Good: Verify JWT signature
async function getUser(req: Request): Promise<Response> {
let token = req.headers.get("authorization")?.split(" ")[1];
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
let payload = await verifyJWT(token, process.env.JWT_SECRET!);
let user = await db.users.findUnique({
where: { id: payload.sub },
});
return Response.json(user);
}
// Good: Verify file integrity with checksum
async function downloadUpdate(req: Request): Promise<Response> {
let file = await fetch("https://cdn.example.com/update.zip");
let buffer = await file.arrayBuffer();
let hash = crypto
.createHash("sha256")
.update(Buffer.from(buffer))
.digest("hex");
let expected = "a1b2c3d4..."; // From trusted source
if (hash !== expected) {
return new Response("Integrity check failed", { status: 400 });
}
return new Response(buffer);
}
// Good: Signed cookies
function signCookie(value: string, secret: string): string {
let sig = crypto.createHmac("sha256", secret).update(value).digest("hex");
return `${value}.${sig}`;
}
function verifyCookie(signedValue: string, secret: string): string | null {
let [value, signature] = signedValue.split(".");
let expected = crypto
.createHmac("sha256", secret)
.update(value)
.digest("hex");
return signature === expected ? value : null;
}Rules
1. Always verify JWT signatures - Never decode without verification 2. Never trust client data - Look up prices, roles, permissions server-side 3. Use JSON.parse, never eval - Safe deserialization only 4. Use Subresource Integrity - For all CDN-loaded scripts/styles 5. Sign cookies - Use HMAC for tamper detection 6. Verify checksums - For downloaded code and updates 7. Lock dependency versions - Use lockfiles to ensure integrity 8. Sign code in CI/CD - Verify builds haven't been tampered with
File Upload Security
Check for secure file upload handling including type validation, size limits, and safe storage.
Related: Path traversal is also covered in injection-attacks.md and broken-access-control.md. XSS prevention is covered in injection-attacks.md and security-headers.md.
Why
- Malware upload: Attackers upload malicious files
- Path traversal: Overwrite system files
- XSS via files: SVG/HTML files execute scripts
- Resource exhaustion: Huge file uploads
What to Check
Vulnerability Indicators:
- [ ] No file type validation
- [ ] No file size limits
- [ ] Original filename used for storage
- [ ] Files stored in web-accessible directory
- [ ] No MIME type validation
- [ ] Both extension and MIME type not checked
Bad Patterns
// Bad: No validation
async function uploadFile(req: Request): Promise<Response> {
let formData = await req.formData();
let file = formData.get("file") as File;
// No type or size checking!
await writeFile(`./uploads/${file.name}`, file);
return new Response("Uploaded");
}
// Bad: Using original filename
await writeFile(`./public/uploads/${file.name}`, buffer);
// User could upload "../../etc/passwd"Good Patterns
// Good: Comprehensive file validation
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"];
const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
async function uploadFile(req: Request): Promise<Response> {
let formData = await req.formData();
let file = formData.get("file") as File;
if (!file) {
return new Response("No file provided", { status: 400 });
}
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
return new Response("Invalid file type", { status: 400 });
}
if (file.size > MAX_FILE_SIZE) {
return new Response("File too large", { status: 400 });
}
let ext = path.extname(file.name).toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
return new Response("Invalid file extension", { status: 400 });
}
// Generate safe random filename
let randomName = crypto.randomBytes(16).toString("hex");
let safeFilename = `${randomName}${ext}`;
// Store outside web root
let uploadPath = path.join(process.cwd(), "private", "uploads", safeFilename);
let buffer = await file.arrayBuffer();
await writeFile(uploadPath, Buffer.from(buffer));
// Store metadata
let uploadedFile = await db.file.create({
data: {
filename: safeFilename,
originalName: file.name.slice(0, 255),
mimeType: file.type,
size: file.size,
uploadedAt: new Date(),
},
});
return Response.json(uploadedFile, { status: 201 });
}Rules
1. Validate MIME type - Check file.type 2. Validate extension - Check file extension 3. Enforce size limits - Prevent huge uploads 4. Generate random filenames - Don't use user input 5. Store outside web root - Not in public/ 6. Validate both MIME and extension - Double check
Injection Attack Prevention
Check for SQL injection, XSS, NoSQL injection, Command injection, and Path Traversal through proper input validation and output encoding.
Related: XSS headers in security-headers.md. File upload path traversal in file-upload-security.md.
Why
- Data breach: SQL/NoSQL injection exposes entire databases
- Account takeover: XSS steals session cookies and credentials
- Remote code execution: Command injection compromises servers
- Data manipulation: Unauthorized modification or deletion
What to Check
- [ ] String concatenation or template literals in database queries
- [ ] User input rendered in HTML without escaping
- [ ] User input passed to shell commands (
exec,spawnwithshell: true) - [ ] User input used in file paths without validation
- [ ] Dynamic code execution (
eval,Functionconstructor,setTimeoutwith strings) - [ ]
dangerouslySetInnerHTMLor.innerHTMLwith user content - [ ] NoSQL queries accepting raw objects with
$where,$regex,$neoperators
Bad Patterns
// Bad: SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Bad: XSS via dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: comment }} />
// Bad: Command injection
execSync(`convert ${filename} output.jpg`);
// Bad: Path traversal
const content = await fs.readFile(`./uploads/${filename}`, "utf-8");Good Patterns
// Good: Parameterized query
async function getUser(req: Request): Promise<Response> {
let url = new URL(req.url);
let email = url.searchParams.get("email");
let user = await db.users.findUnique({ where: { email } });
return Response.json(user);
}
// Good: React auto-escapes by default
function UserComment({ comment }: { comment: string }) {
return <div>{comment}</div>;
}
// Good: Avoid shell commands, validate strictly
async function convertImage(req: Request): Promise<Response> {
let formData = await req.formData();
let file = formData.get("file") as File;
let ALLOWED = ["image/jpeg", "image/png", "image/webp"];
if (!ALLOWED.includes(file.type)) {
return new Response("Invalid type", { status: 400 });
}
let buffer = await file.arrayBuffer();
// Use image library, not shell
return new Response("Uploaded", { status: 200 });
}
// Good: Allowlist for file paths
async function readFile(req: Request): Promise<Response> {
let url = new URL(req.url);
let filename = url.searchParams.get("file");
let ALLOWED = ["terms.pdf", "privacy.pdf", "guide.pdf"];
if (!filename || !ALLOWED.includes(filename) || filename.includes("..")) {
return new Response("Invalid file", { status: 400 });
}
let content = await fs.readFile(`./documents/${filename}`, "utf-8");
return new Response(content);
}Rules
1. Always use parameterized queries - Never concatenate user input into SQL 2. Validate all input - Use type checks and format validation 3. Escape output by context - HTML, JavaScript, SQL require different escaping 4. Use allowlists over denylists - Explicitly allow known-good values 5. Never use eval() - Find safe alternatives for dynamic execution 6. Avoid shell commands - Use libraries or built-in APIs instead 7. Validate file paths - Prevent directory traversal with strict validation
Insecure Design
Check for security anti-patterns and flaws in application architecture that can't be fixed by implementation alone.
Why
- Fundamental flaws: Can't be patched, require redesign
- Business logic bypass: Attackers exploit workflow flaws
- Privilege escalation: Design allows unauthorized access
- Data corruption: Race conditions and logic errors
What to Check
Vulnerability Indicators:
- [ ] Security by obscurity instead of proper access control
- [ ] Missing rate limiting on expensive operations
- [ ] No input validation on business logic
- [ ] Race conditions in multi-step workflows
- [ ] Trust boundaries not defined
- [ ] Missing defense in depth
- [ ] No threat modeling performed
Bad Patterns
// Bad: Security by obscurity
if (req.headers.get("x-admin-secret") === "admin123") {
// Admin operations
}
// Bad: Race condition in balance check
const balance = await getBalance(from);
if (balance >= amount) {
// Race: balance could change here!
await updateBalance(from, balance - amount);
}
// Bad: No rate limiting
async function generateReport(req: Request): Promise<Response> {
const report = await runExpensiveQuery(); // Can DoS
return new Response(report);
}
// Bad: Trust user role from client
const { isAdmin } = await req.json();
if (isAdmin) {
await db.users.delete({ where: { id } }); // User can claim admin!
}Good Patterns
// Good: Proper RBAC
async function adminEndpoint(req: Request): Promise<Response> {
let session = await getSession(req);
let user = await db.users.findUnique({
where: { id: session.userId },
select: { role: true },
});
if (user.role !== "ADMIN") {
return new Response("Forbidden", { status: 403 });
}
// Admin operations
}
// Good: Transaction for atomic operations
async function transferMoney(from: string, to: string, amount: number) {
await db.$transaction(async (tx) => {
let fromAccount = await tx.account.findUnique({
where: { id: from },
select: { balance: true },
});
if (!fromAccount || fromAccount.balance < amount) {
throw new Error("Insufficient funds");
}
await tx.account.update({
where: { id: from },
data: { balance: { decrement: amount } },
});
await tx.account.update({
where: { id: to },
data: { balance: { increment: amount } },
});
});
}
// Good: Rate limiting on expensive operations
async function generateReport(req: Request): Promise<Response> {
let session = await getSession(req);
let { success } = await reportLimit.limit(session.userId);
if (!success) {
return new Response("Rate limit exceeded", { status: 429 });
}
let report = await runExpensiveQuery();
return new Response(report);
}
// Good: Server-side role verification
async function deleteUser(req: Request): Promise<Response> {
let session = await getSession(req);
let user = await db.users.findUnique({
where: { id: session.userId },
select: { role: true },
});
if (user.role !== "ADMIN") {
return new Response("Forbidden", { status: 403 });
}
let { targetUserId } = await req.json();
await db.users.delete({ where: { id: targetUserId } });
return new Response("Deleted");
}Rules
1. Don't rely on security by obscurity - Use proper authentication 2. Use transactions for atomic operations - Prevent race conditions 3. Rate limit expensive operations - Prevent resource exhaustion 4. Verify privileges server-side - Never trust client data 5. Implement defense in depth - Multiple layers of security 6. Perform threat modeling - Identify risks in design phase 7. Define trust boundaries - Know what to validate 8. Fail securely - Default deny, not default allow
Security Logging and Monitoring Failures
Check for insufficient logging of security events, missing monitoring, and lack of incident response capabilities.
Related: Preventing sensitive data in logs is covered in sensitive-data-exposure.md.
Why
- Delayed breach detection: Attacks go unnoticed for months
- No audit trail: Can't investigate incidents
- Compliance violations: Regulations require logging
- Unable to respond: No visibility into attacks
What to Check
Vulnerability Indicators:
- [ ] No logging of authentication attempts
- [ ] Sensitive data in logs (passwords, tokens)
- [ ] No monitoring or alerting on suspicious activity
- [ ] Logs not retained long enough
- [ ] No log integrity protection
- [ ] Missing request IDs for tracing
Bad Patterns
// Bad: No logging of security events
async function login(req: Request): Promise<Response> {
let { email, password } = await req.json();
let user = await authenticate(email, password);
if (!user) {
// No logging of failed attempt
return new Response("Invalid credentials", { status: 401 });
}
return createSession(user);
}
// Bad: Logging sensitive data
console.log("User data:", {
email,
password, // Don't log passwords!
creditCard,
});
// Bad: No structured logging
console.log("User logged in");Good Patterns
// Good: Log security events with context
async function login(req: Request): Promise<Response> {
let { email, password } = await req.json();
let ip = req.headers.get("x-forwarded-for");
let user = await authenticate(email, password);
if (!user) {
logger.warn("Failed login", {
email,
ip,
timestamp: new Date().toISOString(),
});
return new Response("Invalid credentials", { status: 401 });
}
logger.info("Successful login", { userId: user.id, email, ip });
return createSession(user);
}
// Good: Structured logging with sanitization
function createLogger() {
let sensitiveKeys = ["password", "token", "secret", "apiKey"];
function sanitize(obj: any): any {
if (typeof obj !== "object" || obj === null) return obj;
let sanitized: any = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sensitiveKeys.some((sk) =>
key.toLowerCase().includes(sk),
)
? "[REDACTED]"
: typeof value === "object"
? sanitize(value)
: value;
}
return sanitized;
}
return {
info(message: string, context?: Record<string, unknown>) {
console.log(
JSON.stringify({
level: "info",
message,
context: context ? sanitize(context) : undefined,
timestamp: new Date().toISOString(),
}),
);
},
warn(message: string, context?: Record<string, unknown>) {
console.warn(
JSON.stringify({
level: "warn",
message,
context: context ? sanitize(context) : undefined,
timestamp: new Date().toISOString(),
}),
);
},
error(message: string, error: Error, context?: Record<string, unknown>) {
console.error(
JSON.stringify({
level: "error",
message,
context: {
error: error.message,
stack: error.stack,
...sanitize(context || {}),
},
timestamp: new Date().toISOString(),
}),
);
},
};
}
const logger = createLogger();Rules
1. Log all authentication events - Successes and failures 2. Log authorization failures - When access is denied 3. Don't log sensitive data - Sanitize passwords, tokens, PII 4. Use structured logging - JSON format for parsing 5. Include context - User ID, IP, timestamp, request ID 6. Monitor and alert - Set up alerts for suspicious patterns 7. Retain logs appropriately - Balance storage and compliance 8. Protect log integrity - Prevent tampering
Rate Limiting and DoS Prevention
Check for rate limiting on authentication endpoints, APIs, and resource-intensive operations to prevent abuse and denial of service.
Related: Authentication rate limiting is covered in authentication-failures.md. API rate limiting is covered in api-security.md.
Why
- Brute force prevention: Stop password guessing attacks
- Resource exhaustion: Prevent server overload
- Cost control: Limit API abuse and costs
- Fair usage: Ensure availability for all users
What to Check
Vulnerability Indicators:
- [ ] No rate limiting on login/signup endpoints
- [ ] No rate limiting on password reset
- [ ] Unlimited API requests
- [ ] No throttling on expensive operations
- [ ] Missing 429 (Too Many Requests) responses
Bad Patterns
// Bad: No rate limiting on login
async function login(req: Request): Promise<Response> {
let { email, password } = await req.json();
// Allows unlimited login attempts
let user = await authenticate(email, password);
if (!user) {
return new Response("Invalid credentials", { status: 401 });
}
return createSession(user);
}
// Bad: No API rate limiting
async function apiEndpoint(req: Request): Promise<Response> {
// Can be called unlimited times
let data = await expensiveQuery();
return Response.json(data);
}Good Patterns
// Good: Rate limiting with Redis
const loginRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, "15m"), // 5 attempts per 15 min
analytics: true,
});
async function login(req: Request): Promise<Response> {
let ip = req.headers.get("x-forwarded-for") || "unknown";
let { success, limit, remaining, reset } = await loginRateLimit.limit(ip);
if (!success) {
return new Response("Too many login attempts", {
status: 429,
headers: {
"Retry-After": String(Math.ceil((reset - Date.now()) / 1000)),
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": String(remaining),
"X-RateLimit-Reset": String(reset),
},
});
}
let { email, password } = await req.json();
let user = await authenticate(email, password);
if (!user) {
return new Response("Invalid credentials", { status: 401 });
}
return createSession(user);
}
// Good: Per-user API rate limiting
const apiRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, "1h"),
});
async function apiEndpoint(req: Request): Promise<Response> {
let session = await getSession(req);
if (!session) return new Response("Unauthorized", { status: 401 });
let { success } = await apiRateLimit.limit(session.userId);
if (!success) return new Response("Rate limit exceeded", { status: 429 });
let data = await performOperation();
return Response.json(data);
}
// Good: Tiered rate limiting
function getRateLimit(tier: string): Ratelimit {
let limits = {
free: Ratelimit.slidingWindow(10, "1h"),
pro: Ratelimit.slidingWindow(100, "1h"),
enterprise: Ratelimit.slidingWindow(1000, "1h"),
};
return new Ratelimit({ redis, limiter: limits[tier] || limits.free });
}Rules
1. Rate limit auth endpoints - Prevent brute force 2. Per-IP and per-user limits - Multiple layers 3. Return 429 status - Standard rate limit response 4. Include retry headers - Retry-After, X-RateLimit-\ 5. Different limits for tiers - Free vs paid users 6. Rate limit expensive operations* - Reports, exports, search
Open Redirect Prevention
Check for unvalidated redirect and forward URLs that could be used for phishing attacks.
Related: SSRF prevention (server-side URL validation) is covered in ssrf-attacks.md.
Why
- Phishing attacks: Legitimate domain redirects to malicious site
- Credential theft: Users trust your domain and enter credentials
- OAuth attacks: Redirect after auth to steal tokens
- Trust abuse: Your domain's reputation exploited
What to Check
Vulnerability Indicators:
- [ ] Redirect URLs from query parameters
- [ ] No validation of redirect target
- [ ] External redirects allowed without warning
- [ ] OAuth return_uri not validated
Bad Patterns
// Bad: Unvalidated redirect
async function callback(req: Request): Promise<Response> {
let url = new URL(req.url);
let returnUrl = url.searchParams.get("return");
// Attacker can set return=https://evil.com
return Response.redirect(returnUrl!);
}
// Bad: No validation on OAuth callback
async function oauthCallback(req: Request): Promise<Response> {
let url = new URL(req.url);
let redirectUri = url.searchParams.get("redirect_uri");
// Complete OAuth flow...
return Response.redirect(redirectUri!);
}Good Patterns
// Good: Validate against allowlist
const ALLOWED_REDIRECTS = ["/dashboard", "/profile", "/settings"];
async function callback(req: Request): Promise<Response> {
let url = new URL(req.url);
let returnUrl = url.searchParams.get("return") || "/";
if (!ALLOWED_REDIRECTS.includes(returnUrl)) {
return Response.redirect("/");
}
return Response.redirect(returnUrl);
}
// Good: Validate URL is relative
function isValidRedirect(url: string): boolean {
return url.startsWith("/") && !url.startsWith("//");
}
async function callback(req: Request): Promise<Response> {
let url = new URL(req.url);
let returnUrl = url.searchParams.get("return") || "/";
if (!isValidRedirect(returnUrl)) {
return Response.redirect("/");
}
return Response.redirect(returnUrl);
}
// Good: Validate OAuth redirect_uri
const ALLOWED_OAUTH_REDIRECTS = [
"https://app.example.com/callback",
"https://admin.example.com/callback",
];
async function oauthCallback(req: Request): Promise<Response> {
let url = new URL(req.url);
let redirectUri = url.searchParams.get("redirect_uri");
if (!redirectUri || !ALLOWED_OAUTH_REDIRECTS.includes(redirectUri)) {
return new Response("Invalid redirect_uri", { status: 400 });
}
// Complete OAuth flow...
return Response.redirect(redirectUri);
}Rules
1. Validate redirect URLs - Use allowlist 2. Only allow relative URLs - Starts with / not // 3. Never trust user input - For redirect targets 4. Validate OAuth redirects - Pre-registered URIs only 5. Default to safe redirect - Home page if invalid
Secrets Management
Check for hardcoded secrets, exposed API keys, and improper credential management.
Related: Encryption key management in cryptographic-failures.md. Sensitive data exposure in sensitive-data-exposure.md.
Why
- Credential exposure: API keys in code can be stolen
- Repository leaks: Committed secrets in Git history
- Unauthorized access: Exposed keys grant system access
- Compliance violations: Regulations require secret protection
What to Check
- [ ] Hardcoded API keys, passwords, tokens in code
- [ ] Secrets committed to version control
- [ ] .env files committed to repository
- [ ] API keys in client-side code
- [ ] Secrets in logs or error messages
- [ ] No secret rotation policy
Bad Patterns
// Bad: Hardcoded API key
const STRIPE_SECRET_KEY = "sk_live_51H..."; // VULNERABLE!
// Bad: Hardcoded database password
const db = createConnection({
host: "localhost",
user: "admin",
password: "SuperSecret123!" // VULNERABLE!
});
// Bad: Secret in client-side code
const config = {
apiKey: "AIzaSyB..." // VULNERABLE: Exposed in browser
};
// Bad: .env file committed to Git
// .env (in repository) - VULNERABLE!
DATABASE_URL=postgresql://user:password@localhost/db
API_SECRET=my-secret-key
// Bad: Logging secrets
console.log("Connecting with API key:", process.env.API_KEY);Good Patterns
// Good: Use environment variables
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
if (!STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY not set");
}
// Good: Validate env vars at startup
function validateEnv() {
let required = ["DATABASE_URL", "JWT_SECRET", "STRIPE_SECRET_KEY"];
let missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing env vars: ${missing.join(", ")}`);
}
}
// Good: Add .env to .gitignore (never commit secrets)
// Good: Provide .env.example for documentation (safe to commit)
// Good: Secret rotation
async function rotateApiKey(userId: string) {
let newKey = crypto.randomBytes(32).toString("hex");
await db.apiKeys.create({
data: {
userId,
key: newKey,
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
},
});
return newKey;
}
// Good: Use secret management service
async function getSecret(name: string): Promise<string> {
if (process.env.NODE_ENV === "production") {
return await secretsManager.getSecretValue(name);
}
let value = process.env[name];
if (!value) throw new Error(`Secret ${name} not found`);
return value;
}Rules
1. Never hardcode secrets - Use environment variables or secret managers 2. Add .env to .gitignore - Never commit secret files 3. Rotate secrets regularly - Implement expiration and rotation 4. Validate env vars at startup - Fail fast if secrets missing 5. Don't log secrets - Sanitize logs to remove sensitive values 6. No secrets in client code - Keep API keys server-side only 7. Use secret management services - For production (AWS Secrets Manager, Vault, etc.) 8. Scan Git history - Use tools to find accidentally committed secrets
Security Headers
Check for proper HTTP security headers that protect against XSS, clickjacking, MIME sniffing, and downgrade attacks.
Related: XSS input validation in injection-attacks.md. CORS in cors-configuration.md.
Why
- XSS protection: CSP prevents script injection
- Clickjacking prevention: X-Frame-Options stops iframe embedding
- HTTPS enforcement: HSTS ensures encrypted connections
- MIME sniffing attacks: X-Content-Type-Options prevents content confusion
- Information leakage: Referrer-Policy controls referrer data
What to Check
- [ ] Missing Content-Security-Policy header
- [ ] Missing Strict-Transport-Security (HSTS)
- [ ] Missing X-Frame-Options
- [ ] Missing X-Content-Type-Options
- [ ] Overly permissive CSP (
unsafe-inline,unsafe-eval) - [ ] No Permissions-Policy
- [ ] Missing Referrer-Policy
Bad Patterns
// Bad: No security headers
async function handler(req: Request): Promise<Response> {
let html = "<html><body>Hello</body></html>";
// VULNERABLE: Missing all security headers
return new Response(html, {
headers: { "Content-Type": "text/html" },
});
}
// Bad: Permissive CSP
const headers = {
// VULNERABLE: unsafe-inline allows XSS
"Content-Security-Policy": "default-src * 'unsafe-inline' 'unsafe-eval'",
};Good Patterns
// Good: Comprehensive security headers
function getSecurityHeaders(): Record<string, string> {
return {
"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'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
"X-Frame-Options": "DENY",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
};
}
async function handler(req: Request): Promise<Response> {
let html = "<html><body>Hello</body></html>";
return new Response(html, {
headers: {
"Content-Type": "text/html",
...getSecurityHeaders(),
},
});
}
// Good: CSP with nonces for inline scripts
async function renderPage(req: Request): Promise<Response> {
let nonce = crypto.randomBytes(16).toString("base64");
let html = `
<!DOCTYPE html>
<html>
<head>
<script nonce="${nonce}">
console.log('This script is allowed');
</script>
</head>
<body>Content</body>
</html>
`;
return new Response(html, {
headers: {
"Content-Type": "text/html",
"Content-Security-Policy": `default-src 'self'; script-src 'self' 'nonce-${nonce}'`,
},
});
}Rules
1. Always set CSP - Strict policy without unsafe-inline/unsafe-eval 2. Enable HSTS - Minimum 1 year, include subdomains 3. Set X-Frame-Options - Use DENY or SAMEORIGIN 4. Set X-Content-Type-Options - Always nosniff 5. Configure Referrer-Policy - strict-origin-when-cross-origin 6. Use nonces for inline scripts - When inline scripts are needed 7. Set Permissions-Policy - Restrict unnecessary browser features
Security Misconfiguration
Check for insecure default configurations, unnecessary features enabled, verbose error messages, and missing security patches.
Why
- Information disclosure: Verbose errors reveal system details
- Unauthorized access: Default credentials still active
- Attack surface: Unnecessary features expose vulnerabilities
- Known vulnerabilities: Outdated software with public exploits
What to Check
Vulnerability Indicators:
- [ ] Debug mode enabled in production
- [ ] Default credentials not changed
- [ ] Unnecessary features/endpoints enabled
- [ ] Detailed error messages in production
- [ ] Directory listing enabled
- [ ] Outdated dependencies
- [ ] Missing security patches
Bad Patterns
// Bad: Debug mode in production
const DEBUG = true; // Should be from env
if (DEBUG) {
console.log("Detailed system info:", process.env);
}
// Bad: Verbose error messages
catch (error) {
return Response.json({
error: error.message,
stack: error.stack,
query: sqlQuery,
env: process.env
}, { status: 500 });
}
// Bad: Default credentials
const ADMIN_PASSWORD = "admin123";
// Bad: Unnecessary admin endpoints exposed
async function debugInfo(req: Request): Promise<Response> {
return Response.json({
env: process.env,
config: appConfig,
routes: allRoutes
});
}Good Patterns
// Good: Environment-aware configuration
const isProduction = process.env.NODE_ENV === "production";
const config = {
debug: !isProduction,
logLevel: isProduction ? "error" : "debug",
errorDetails: !isProduction
};
// Good: Generic error messages in production
catch (error) {
console.error("Error:", error);
let message = isProduction
? "An error occurred"
: error.message;
return Response.json({ error: message }, { status: 500 });
}
// Good: Strong credentials from environment
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
if (!ADMIN_PASSWORD || ADMIN_PASSWORD.length < 20) {
throw new Error("ADMIN_PASSWORD must be set and strong");
}
// Good: Disable debug endpoints in production
async function debugInfo(req: Request): Promise<Response> {
if (process.env.NODE_ENV === "production") {
return new Response("Not found", { status: 404 });
}
return Response.json({ routes: publicRoutes });
}Rules
1. Disable debug mode in production - No verbose logging or errors 2. Change default credentials - Require strong passwords 3. Disable unnecessary features - Minimize attack surface 4. Generic error messages - Don't reveal system details 5. Keep dependencies updated - Regularly patch vulnerabilities 6. Remove development endpoints - No debug/admin routes in production 7. Secure default configurations - Fail securely by default 8. Regular security audits - npm audit, dependency checks
Sensitive Data Exposure
Check for PII, credentials, and sensitive data exposed in API responses, error messages, logs, or client-side code.
Related: Encryption in cryptographic-failures.md. Secrets in secrets-management.md. Logging in logging-monitoring.md.
Why
- Privacy violation: Exposes users' personal information
- Compliance risk: GDPR, CCPA, HIPAA violations
- Identity theft: PII enables fraud and impersonation
- Credential theft: Exposed secrets enable account takeover
What to Check
- [ ] Password hashes returned in API responses
- [ ] Email, phone, SSN in public endpoints
- [ ] Error messages revealing stack traces or database info
- [ ] Debug information in production
- [ ] API keys, tokens in client-side code
- [ ] Excessive data in responses (return only what's needed)
- [ ] Sensitive data logged to console or files
Bad Patterns
// Bad: Returning all user fields including sensitive data
async function getUser(req: Request): Promise<Response> {
let user = await db.users.findUnique({ where: { id } });
// Returns password hash, email, tokens, etc.
return Response.json(user);
}
// Bad: Logging sensitive data
console.log("User login:", { email, password, creditCard });
// Bad: Exposing internal IDs
return Response.json({
internalUserId: user.id,
databaseId: user.dbId,
});Good Patterns
// Good: Explicit field selection
async function getUser(req: Request): Promise<Response> {
let session = await getSession(req);
let user = await db.users.findUnique({
where: { id: session.userId },
select: {
id: true,
name: true,
avatar: true,
createdAt: true,
// Excludes: password, email, tokens, etc.
},
});
return Response.json(user);
}
// Good: DTO for public profiles
async function getUserProfile(req: Request): Promise<Response> {
let url = new URL(req.url);
let userId = url.searchParams.get("id");
let user = await db.users.findUnique({
where: { id: userId },
select: { id: true, name: true, avatar: true, bio: true },
});
return Response.json(user);
}
// Good: Conditional field exposure
async function getUserProfile(req: Request): Promise<Response> {
let session = await getSession(req);
let url = new URL(req.url);
let userId = url.searchParams.get("id");
let isOwn = session?.userId === userId;
let user = await db.users.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
avatar: true,
bio: true,
email: isOwn,
emailVerified: isOwn,
},
});
return Response.json(user);
}
// Good: Sanitize logs
function sanitizeForLogging(obj: any): any {
let sensitive = ["password", "token", "secret", "apiKey", "creditCard"];
let sanitized = { ...obj };
for (const key of Object.keys(sanitized)) {
if (sensitive.some((s) => key.toLowerCase().includes(s))) {
sanitized[key] = "[REDACTED]";
}
}
return sanitized;
}
console.log("Login attempt:", sanitizeForLogging({ email, password }));
// Output: { email: "user@example.com", password: "[REDACTED]" }Rules
1. Never return password hashes - Even hashed, they can be cracked 2. Use explicit field selection - Don't return entire database records 3. Create DTOs for responses - Define exactly what fields are public 4. Generic error messages - Don't expose system details to users 5. Log full errors server-side - Return generic messages to clients 6. Sanitize logs - Redact passwords, tokens, PII before logging 7. Different views for different users - Own profile vs others' profiles 8. Disable debug in production - No verbose errors or stack traces
Session Security
Check for secure session management including cookie flags, token storage, and session lifecycle.
Related: Authentication is covered in authentication-failures.md. CSRF protection is covered in csrf-protection.md.
Why
- Session hijacking: Attackers steal session tokens
- Session fixation: Attackers set known session ID
- XSS token theft: JavaScript access to tokens
- CSRF attacks: Missing cookie protection
What to Check
Vulnerability Indicators:
- [ ] Cookies missing HttpOnly flag
- [ ] Cookies missing Secure flag
- [ ] Cookies missing SameSite attribute
- [ ] JWT stored in localStorage
- [ ] Sessions never expire
- [ ] Session not regenerated after login
- [ ] Predictable session IDs
Bad Patterns
// Bad: No security flags on cookie
return new Response("OK", {
headers: { "Set-Cookie": `session=${sessionId}` },
});
// Bad: Session never expires
await db.session.create({
data: { id: sessionId, userId }, // No expiresAt!
});
// Bad: Predictable session ID
const sessionId = `${Date.now()}-${Math.random()}`;Good Patterns
// Good: Secure cookie with all flags
async function createSession(userId: string): Promise<Response> {
let sessionId = crypto.randomBytes(32).toString("hex");
await db.session.create({
data: {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
createdAt: new Date(),
},
});
return new Response("OK", {
headers: {
"Set-Cookie": [
`session=${sessionId}`,
"HttpOnly",
"Secure",
"SameSite=Strict",
"Path=/",
"Max-Age=3600",
].join("; "),
},
});
}
// Good: Session validation with expiry
async function validateSession(req: Request): Promise<string | null> {
let sessionId = getCookie(req, "session");
if (!sessionId) return null;
let session = await db.session.findUnique({ where: { id: sessionId } });
if (!session || session.expiresAt < new Date()) {
if (session) await db.session.delete({ where: { id: sessionId } });
return null;
}
// Extend session (sliding expiration)
await db.session.update({
where: { id: sessionId },
data: { expiresAt: new Date(Date.now() + 60 * 60 * 1000) },
});
return session.userId;
}
// Good: Logout invalidates session
async function logout(req: Request): Promise<Response> {
let sessionId = getCookie(req, "session");
if (sessionId) await db.session.delete({ where: { id: sessionId } });
return new Response("OK", {
headers: { "Set-Cookie": "session=; Max-Age=0; Path=/" },
});
}Rules
1. Set HttpOnly flag - Prevent XSS token theft 2. Set Secure flag - HTTPS only 3. Set SameSite=Strict - CSRF protection 4. Use cryptographically random IDs - crypto.randomBytes 5. Set expiration - Both absolute and idle timeout 6. Regenerate on login - Prevent session fixation 7. Don't store in localStorage - Use HttpOnly cookies 8. Validate on every request - Check expiry and validity
Server-Side Request Forgery (SSRF)
Check for unvalidated URLs that allow attackers to make requests to internal services or arbitrary external URLs.
Related: URL validation in redirects is covered in redirect-validation.md.
Why
- Internal network access: Attackers reach internal services
- Cloud metadata exposure: Access to AWS/GCP metadata endpoints
- Port scanning: Map internal network
- Bypass firewall: Access protected resources
What to Check
Vulnerability Indicators:
- [ ] User-provided URLs passed to fetch/axios without validation
- [ ] No allowlist for allowed domains
- [ ] Missing checks for internal IP ranges
- [ ] Webhook URLs not validated
- [ ] URL redirects followed automatically
Bad Patterns
// Bad: Fetching user-provided URL
async function fetchUrl(req: Request): Promise<Response> {
let { url } = await req.json();
// SSRF: Can access internal services!
let response = await fetch(url);
let data = await response.text();
return new Response(data);
}
// Bad: No validation on webhook URL
async function registerWebhook(req: Request): Promise<Response> {
let { webhookUrl } = await req.json();
await db.webhook.create({
data: { url: webhookUrl },
});
// Later: fetch(webhookUrl) - could be internal
}Good Patterns
// Good: Validate against allowlist
const ALLOWED_DOMAINS = ["api.example.com", "cdn.example.com"];
async function fetchUrl(req: Request): Promise<Response> {
let { url } = await req.json();
let parsedUrl = new URL(url);
if (parsedUrl.protocol !== "https:") {
return new Response("Only HTTPS allowed", { status: 400 });
}
if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) {
return new Response("Domain not allowed", { status: 400 });
}
if (isInternalIP(parsedUrl.hostname)) {
return new Response("Internal IPs not allowed", { status: 400 });
}
let response = await fetch(url, { redirect: "manual" });
return new Response(await response.text());
}
function isInternalIP(hostname: string): boolean {
return [
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./,
/^localhost$/i,
].some((range) => range.test(hostname));
}Rules
1. Validate URLs against allowlist - Never trust user URLs 2. Block internal IP ranges - 127.0.0.1, 10.x, 192.168.x, etc. 3. Enforce HTTPS - No HTTP or other protocols 4. Disable redirects - Or validate redirect targets 5. Block cloud metadata - 169.254.169.254 (AWS/GCP/Azure)
Vulnerable and Outdated Dependencies
Check for outdated packages with known security vulnerabilities and supply chain risks.
Why
- Known exploits: Public CVEs make attacks easy
- Supply chain attacks: Compromised packages
- Transitive dependencies: Vulnerabilities deep in dependency tree
- Maintenance risk: Unmaintained packages won't get patches
What to Check
- [ ] Dependencies with known CVEs or security advisories
- [ ] Severely outdated packages (major versions behind current)
- [ ] Packages without recent updates (abandoned/unmaintained)
- [ ] Missing dependency lockfiles
- [ ] Wildcard or loose version constraints in production
- [ ] Unused dependencies bloating the project
- [ ] Development dependencies bundled in production builds
- [ ] Transitive vulnerabilities in indirect dependencies
Bad Patterns
// Bad: Wildcard versions allow unexpected updates
// package.json
{
"dependencies": {
"express": "*", // Any version can be installed
"react": "^18.0.0" // Minor/patch versions can change
}
}
// Bad: No lockfile means versions drift between installs
// Missing: package-lock.json, yarn.lock, pnpm-lock.yaml, etc.
// Bad: Dev dependencies mixed with production
{
"dependencies": {
"express": "4.18.2",
"jest": "29.5.0", // Should be devDependency
"eslint": "8.40.0" // Should be devDependency
}
}Good Patterns
````typescript // Good: Pinned versions with lockfile { "dependencies": { "express": "4.18.2", // Exact version pinned "react": "18.2.0" }, "devDependencies": { "jest": "29.5.0", "eslint": "8.40.0" } } // Plus: Lockfile committed (package-lock.json, yarn.lock, etc.)
// Good: Regular dependency audits in CI/CD // .github/workflows/security.yml
name: Security Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm audit --production # Or: pip-audit, bundle audit, etc.Before installing new packages:
- Check package age and download stats
- Review maintainer history
- Scan for known vulnerabilities
- Verify package scope matches intent (avoid typosquatting)
Rules
1. Always use lockfiles - Commit dependency lockfiles for reproducible builds 2. Pin production versions - Use exact versions for production dependencies 3. Audit regularly - Run security audits in CI/CD and before deployments 4. Keep dependencies updated - Use automated update tools 5. Separate dev dependencies - Keep development tools separate from production 6. Remove unused packages - Regularly clean up unused dependencies 7. Review before adding - Check package age, maintainers, and reputation 8. Monitor advisories - Subscribe to security advisories for critical dependencies
Related skills
How it compares
Use owasp-security-check for REST-specific OWASP patterns; dedicated SAST scanners may miss conversational context about which fields should be assignable.
FAQ
What order should rules be scanned?
CRITICAL auth and data protection first, then HIGH config, then MEDIUM API.
Does it replace manual review?
No; subtle logic and thematic issues still need human judgment.
What app types are supported?
Web apps, REST APIs, SPAs, SSR, and mixed architectures.
Is Owasp Security Check safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.