Security Audit Skill
This skill helps you identify and fix security vulnerabilities in the codebase.
When to Use This Skill
-
Before production deployments
-
When adding authentication/authorization
-
When handling user input
-
After dependency updates
-
During code reviews
-
When integrating third-party services
OWASP Top 10 (2021)
- Broken Access Control
Issue: Users can access resources they shouldn't
Check For:
Search for authorization checks
grep -r "hasPermission|canAccess|isAuthorized" apps/ --include="*.ts"
Look for missing auth checks
grep -r "export.*function|export.async function" apps/api/src/routes --include=".ts"
Example Vulnerability:
// ❌ No authorization check export async function deletePost(postId: string) { await db.delete(posts).where(eq(posts.id, postId)); }
// ✅ With authorization export async function deletePost(postId: string, userId: string) { const post = await db.query.posts.findFirst({ where: eq(posts.id, postId), });
if (post.authorId !== userId) { throw new Error("Unauthorized"); }
await db.delete(posts).where(eq(posts.id, postId)); }
- Cryptographic Failures
Issue: Sensitive data exposed or poorly encrypted
Check For:
Look for hardcoded secrets
grep -ri "password.=|api[_-]key.=|secret.=" apps/ packages/ --include=".ts"
Check for sensitive data in logs
grep -r "console.log" apps/ --include="*.ts" | grep -i "password|token|secret"
Example Vulnerability:
// ❌ Storing passwords in plain text await db.insert(users).values({ email, password, // Plain text! });
// ✅ Hashing passwords import bcrypt from "bcrypt";
const hashedPassword = await bcrypt.hash(password, 10); await db.insert(users).values({ email, password: hashedPassword, });
- Injection
Issue: SQL injection, command injection, etc.
Check For:
Look for string concatenation in queries
grep -r "SELECT.${" apps/ packages/ --include=".ts" grep -r "WHERE.${" apps/ packages/ --include=".ts"
Check for eval usage
grep -r "eval(" apps/ packages/ --include="*.ts"
Example Vulnerability:
// ❌ SQL Injection
const query = SELECT * FROM users WHERE id = ${userId};
// ✅ Parameterized query (Drizzle ORM) const user = await db.query.users.findFirst({ where: eq(users.id, userId), });
// ❌ Command Injection
const result = exec(git log ${userInput});
// ✅ Sanitized input import { z } from "zod";
const schema = z.string().regex(/^[a-zA-Z0-9-]+$/);
const sanitized = schema.parse(userInput);
const result = exec(git log ${sanitized});
- Insecure Design
Issue: Flawed security architecture
Check For:
-
Missing rate limiting
-
No input validation
-
Weak session management
-
Missing CSRF protection
Example Vulnerability:
// ❌ No rate limiting export async function login(email: string, password: string) { // Anyone can brute force passwords const user = await verifyCredentials(email, password); return createSession(user); }
// ✅ With rate limiting import { Ratelimit } from "@upstash/ratelimit"; import { redis } from "@sgcarstrends/utils";
const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 attempts per 15 min });
export async function login(email: string, password: string, ip: string) { const { success } = await ratelimit.limit(ip);
if (!success) { throw new Error("Too many login attempts"); }
const user = await verifyCredentials(email, password); return createSession(user); }
- Security Misconfiguration
Issue: Insecure default configs, unnecessary services
Check For:
Look for debug mode in production
grep -r "debug.true" apps/ --include=".ts" --include="*.json"
Check for exposed error messages
grep -r "error.stack|error.message" apps/ --include="*.ts"
Example Vulnerability:
// ❌ Exposing stack traces in production export async function handler(req: Request) { try { // ... } catch (error) { return Response.json({ error: error.stack }, { status: 500 }); } }
// ✅ Safe error handling export async function handler(req: Request) { try { // ... } catch (error) { console.error("Error:", error); // Log internally return Response.json( { error: "Internal server error" }, // Generic message { status: 500 } ); } }
- Vulnerable Components
Issue: Using outdated or vulnerable dependencies
Check For:
Audit dependencies
pnpm audit
Check for outdated packages
pnpm outdated
Look for specific vulnerable packages
pnpm list | grep "package-name"
Fix:
Update vulnerable packages
pnpm update package-name
Or update all
pnpm update -r
Check audit after update
pnpm audit
- Authentication Failures
Issue: Weak authentication mechanisms
Check For:
Look for weak password requirements
grep -r "password.length" apps/ --include=".ts"
Check for missing password hashing
grep -r "password.=" apps/ --include=".ts" | grep -v "bcrypt|argon2|hash"
Example Vulnerability:
// ❌ Weak password validation const passwordSchema = z.string().min(6);
// ✅ Strong password validation const passwordSchema = z.string() .min(12) .regex(/[A-Z]/, "Must contain uppercase") .regex(/[a-z]/, "Must contain lowercase") .regex(/[0-9]/, "Must contain number") .regex(/[^A-Za-z0-9]/, "Must contain special character");
- Software and Data Integrity Failures
Issue: Unverified updates, insecure CI/CD
Check For:
Look for unsigned packages
grep -r "npm install|pnpm add" .github/ --include="*.yml"
Check for pinned versions
cat package.json | grep -v "^\s*".":\s"[^~]"
Example Fix:
// ❌ Unpinned versions { "dependencies": { "react": "^18.0.0", // Could install 18.9.9 "next": "~15.0.0" // Could install 15.0.9 } }
// ✅ Pinned versions (with pnpm catalog) { "dependencies": { "react": "catalog:", // Exact version from catalog "next": "catalog:" } }
- Logging and Monitoring Failures
Issue: Insufficient logging, no alerting
Check For:
Look for authentication logging
grep -r "login|authenticate" apps/api --include="*.ts" | grep -c "log|console"
Check for error logging
grep -r "catch.error" apps/ --include=".ts" | grep -v "log|console"
Example Vulnerability:
// ❌ No logging export async function login(email: string, password: string) { const user = await verifyCredentials(email, password); return createSession(user); }
// ✅ With security logging export async function login(email: string, password: string, ip: string) { try { const user = await verifyCredentials(email, password);
// Log successful login
console.log(`Login success: ${email} from ${ip}`);
return createSession(user);
} catch (error) {
// Log failed attempt
console.warn(Login failed: ${email} from ${ip});
throw error;
}
}
- Server-Side Request Forgery (SSRF)
Issue: Server makes requests to attacker-controlled URLs
Check For:
Look for user-controlled URLs
grep -r "fetch(.*req|axios(.req" apps/ --include=".ts"
Example Vulnerability:
// ❌ SSRF vulnerability export async function fetchUrl(url: string) { return await fetch(url); // User controls URL! }
// ✅ Whitelist approach const ALLOWED_DOMAINS = ["api.example.com", "data.gov.sg"];
export async function fetchUrl(url: string) { const parsedUrl = new URL(url);
if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) { throw new Error("Domain not allowed"); }
return await fetch(url); }
Input Validation
Always Validate User Input
import { z } from "zod";
// Define schema const userInputSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().min(0).max(150), website: z.string().url().optional(), });
// Validate export async function createUser(data: unknown) { const validated = userInputSchema.parse(data); // Throws if invalid // Now safe to use validated data }
Sanitize HTML
import sanitizeHtml from "sanitize-html";
export function sanitizeUserInput(html: string): string { return sanitizeHtml(html, { allowedTags: ["b", "i", "em", "strong", "a", "p"], allowedAttributes: { a: ["href"], }, }); }
XSS Prevention
React Automatic Escaping
// ✅ Safe - React escapes by default <div>{userInput}</div>
// ❌ Dangerous <div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ Safe if sanitized <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(userInput) }} />
URL Sanitization
// ❌ XSS via javascript: protocol <a href={userUrl}>Click</a>
// ✅ Validate URL function isSafeUrl(url: string): boolean { try { const parsed = new URL(url); return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch { return false; } }
<a href={isSafeUrl(userUrl) ? userUrl : "#"}>Click</a>
CORS Configuration
// ❌ Too permissive app.use(cors({ origin: "*", // Allows any origin! }));
// ✅ Whitelist specific origins app.use(cors({ origin: [ "https://sgcarstrends.com", "https://staging.sgcarstrends.com", process.env.NODE_ENV === "development" ? "http://localhost:3001" : "", ].filter(Boolean), credentials: true, }));
Environment Variables
Never Commit Secrets
Check for committed secrets
git log -p | grep -i "password|secret|key" | head -20
Use git-secrets to prevent commits
git secrets --scan
Use Environment Variables
// ❌ Hardcoded secret const apiKey = "sk_live_abc123";
// ✅ From environment const apiKey = process.env.API_KEY!;
// ✅ With validation const envSchema = z.object({ API_KEY: z.string().min(1), DATABASE_URL: z.string().url(), });
const env = envSchema.parse(process.env);
Security Headers
// next.config.js const securityHeaders = [ { key: "X-DNS-Prefetch-Control", value: "on", }, { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload", }, { key: "X-Frame-Options", value: "SAMEORIGIN", }, { key: "X-Content-Type-Options", value: "nosniff", }, { key: "X-XSS-Protection", value: "1; mode=block", }, { key: "Referrer-Policy", value: "origin-when-cross-origin", }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()", }, ];
module.exports = { async headers() { return [ { source: "/:path*", headers: securityHeaders, }, ]; }, };
Automated Security Scanning
npm/pnpm Audit
Check for vulnerabilities
pnpm audit
Fix automatically
pnpm audit --fix
Get JSON report
pnpm audit --json > audit-report.json
Snyk
Install Snyk
npm install -g snyk
Authenticate
snyk auth
Test for vulnerabilities
snyk test
Monitor project
snyk monitor
OWASP Dependency Check
Run dependency check
dependency-check --project sgcarstrends --scan .
Security Testing
Test Authentication
// tests/security/auth.test.ts describe("Authentication Security", () => { it("rejects invalid credentials", async () => { const response = await login("user@example.com", "wrong-password"); expect(response.status).toBe(401); });
it("rate limits login attempts", async () => { const attempts = Array(10).fill(null).map(() => login("user@example.com", "wrong-password") );
await Promise.all(attempts);
const response = await login("user@example.com", "wrong-password");
expect(response.status).toBe(429); // Too many requests
});
it("does not leak user existence", async () => { const response1 = await login("exists@example.com", "wrong"); const response2 = await login("noexist@example.com", "wrong");
// Same error message for both
expect(response1.message).toBe(response2.message);
}); });
Test Input Validation
describe("Input Validation", () => { it("rejects SQL injection attempts", async () => { const malicious = "'; DROP TABLE users; --";
await expect(
createUser({ name: malicious })
).rejects.toThrow();
});
it("rejects XSS attempts", async () => { const xss = "<script>alert('xss')</script>";
const result = await createPost({ content: xss });
expect(result.content).not.toContain("<script>");
}); });
Security Checklist
Before deployment:
-
All user input validated
-
SQL injection prevented (using ORM)
-
XSS prevented (React escaping, sanitization)
-
CSRF protection enabled
-
Authentication implemented correctly
-
Authorization checks in place
-
Passwords hashed (bcrypt/argon2)
-
Rate limiting configured
-
Security headers set
-
CORS configured properly
-
HTTPS enforced
-
Dependencies audited
-
Secrets in environment variables
-
Error messages don't leak info
-
Logging enabled for security events
References
-
OWASP Top 10: https://owasp.org/www-project-top-ten
-
OWASP Cheat Sheets: https://cheatsheetseries.owasp.org
-
Node.js Security: https://nodejs.org/en/docs/guides/security
-
Related files:
-
Root CLAUDE.md - Security guidelines
Best Practices
-
Validate Everything: Never trust user input
-
Use ORM: Prevent SQL injection
-
Hash Passwords: Use bcrypt or argon2
-
Rate Limit: Prevent brute force
-
Security Headers: Set proper headers
-
HTTPS Only: Enforce HTTPS everywhere
-
Audit Dependencies: Regularly check for vulnerabilities
-
Least Privilege: Grant minimum necessary permissions