Backend security
Security auditing for code vulnerabilities (OWASP Top 10, XSS, SQL injection) and dependency scanning (pnpm audit, Snyk). Use when handling user input, adding authentication, before deployments, or resolving CVEs.
install
source · Clone the upstream repo
git clone https://github.com/sgcarstrends/sgcarstrends
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/sgcarstrends/sgcarstrends "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/security" ~/.claude/skills/sgcarstrends-backend-security && rm -rf "$T"
manifest:
.claude/skills/security/SKILL.mdsource content
Security Skill
Dependency Scanning
pnpm audit
# Run audit pnpm audit # Only high/critical pnpm audit --audit-level=high # Auto-fix pnpm audit --fix # JSON report pnpm audit --json > audit.json
Fix Vulnerabilities
Direct dependencies: Update version in
pnpm-workspace.yaml catalog
Transitive dependencies:
# Find dependency chain pnpm why vulnerable-package # Use overrides as last resort # package.json { "pnpm": { "overrides": { "vulnerable-package": "^3.1.0" } } }
Snyk
snyk auth # Authenticate snyk test # Test for vulnerabilities snyk monitor # Monitor for new vulnerabilities snyk fix # Auto-fix
OWASP Top 10 Checks
1. Broken Access Control
// ❌ No authorization 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)); }
2. Injection Prevention
// ❌ 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) });
3. XSS Prevention
React escapes content by default. When rendering HTML:
- Sanitize with
library before renderingsanitize-html - Never render untrusted content directly
4. Rate Limiting
import { Ratelimit } from "@upstash/ratelimit"; import { redis } from "@sgcarstrends/utils"; const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(5, "15 m"), }); 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"); return verifyCredentials(email, password); }
5. Password Security
import bcrypt from "bcrypt"; // ✅ Hash passwords const hashedPassword = await bcrypt.hash(password, 10); // ✅ 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");
6. SSRF Prevention
// ❌ SSRF vulnerability export async function fetchUrl(url: string) { return await fetch(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
import { z } from "zod"; const userInputSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().min(0).max(150), }); export async function createUser(data: unknown) { const validated = userInputSchema.parse(data); // Now safe to use }
CORS Configuration
// ❌ Too permissive app.use(cors({ 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, }));
Security Headers
// next.config.js const securityHeaders = [ { 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" }, ]; module.exports = { async headers() { return [{ source: "/:path*", headers: securityHeaders }]; }, };
Environment Variables
// ❌ Hardcoded secret const apiKey = "sk_live_EXAMPLE_NOT_REAL"; // ✅ From environment with validation import { z } from "zod"; const envSchema = z.object({ API_KEY: z.string().min(1), DATABASE_URL: z.string().url(), }); const env = envSchema.parse(process.env);
CI Integration
# .github/workflows/security.yml name: Security Audit on: push: branches: [main] schedule: - cron: '0 0 * * 1' # Weekly jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 - run: pnpm install - run: pnpm audit --audit-level=high
Security Checklist
- All user input validated (Zod schemas)
- SQL injection prevented (using ORM)
- XSS prevented (React escaping, sanitization)
- Authentication implemented correctly
- Authorization checks in place
- Passwords hashed (bcrypt/argon2)
- Rate limiting configured
- Security headers set
- CORS configured properly
- HTTPS enforced
- Dependencies audited (pnpm audit)
- Secrets in environment variables
- Error messages don't leak info
References
- OWASP Top 10: https://owasp.org/www-project-top-ten
- pnpm Audit: https://pnpm.io/cli/audit
- Snyk: https://snyk.io