$npx -y skills add sabahattink/antigravity-fullstack-hq --skill security-checklistOWASP Top 10, input validation, SQL injection prevention, rate limiting, CORS. Use when reviewing code for security issues, setting up a new API, or doing a pre-deploy security audit.
| 1 | # Security Checklist |
| 2 | |
| 3 | ## OWASP Top 10 — Quick Reference |
| 4 | |
| 5 | | # | Risk | Mitigation | |
| 6 | |---|------|-----------| |
| 7 | | A01 | Broken Access Control | Always authorize, not just authenticate | |
| 8 | | A02 | Cryptographic Failures | TLS everywhere, bcrypt passwords, no MD5/SHA1 | |
| 9 | | A03 | Injection | Parameterized queries, ORM, input validation | |
| 10 | | A04 | Insecure Design | Threat model, rate limiting, abuse cases | |
| 11 | | A05 | Security Misconfiguration | Disable defaults, review headers | |
| 12 | | A06 | Vulnerable Components | `npm audit`, `pnpm audit`, Dependabot | |
| 13 | | A07 | Auth Failures | MFA, account lockout, secure sessions | |
| 14 | | A08 | Integrity Failures | Verify build artifacts, signed commits | |
| 15 | | A09 | Logging Failures | Log auth events, anomaly detection | |
| 16 | | A10 | SSRF | Validate URLs, block internal IPs | |
| 17 | |
| 18 | ## Input Validation |
| 19 | |
| 20 | ```typescript |
| 21 | // ALWAYS validate at every system boundary using Zod or class-validator |
| 22 | |
| 23 | // NestJS — enable globally |
| 24 | app.useGlobalPipes( |
| 25 | new ValidationPipe({ |
| 26 | whitelist: true, // strip unknown properties |
| 27 | forbidNonWhitelisted: true, // throw on unknown props |
| 28 | transform: true, // auto-coerce types |
| 29 | transformOptions: { |
| 30 | enableImplicitConversion: false, |
| 31 | }, |
| 32 | }) |
| 33 | ) |
| 34 | |
| 35 | // Zod at service boundaries |
| 36 | const CreateOrderSchema = z.object({ |
| 37 | userId: z.string().uuid(), |
| 38 | items: z.array(z.object({ |
| 39 | productId: z.string().uuid(), |
| 40 | quantity: z.number().int().min(1).max(100), |
| 41 | })).min(1).max(50), |
| 42 | couponCode: z.string().max(20).optional(), |
| 43 | }) |
| 44 | |
| 45 | type CreateOrderInput = z.infer<typeof CreateOrderSchema> |
| 46 | |
| 47 | // Parse — throws ZodError on invalid input |
| 48 | const input = CreateOrderSchema.parse(rawBody) |
| 49 | ``` |
| 50 | |
| 51 | ## SQL Injection Prevention |
| 52 | |
| 53 | ```typescript |
| 54 | // ALWAYS use parameterized queries / ORM — never string concatenate |
| 55 | |
| 56 | // Bad — SQL injection possible |
| 57 | const result = await db.query( |
| 58 | `SELECT * FROM users WHERE email = '${email}'` |
| 59 | ) |
| 60 | |
| 61 | // Good — TypeORM parameterized |
| 62 | const user = await this.repo.findOne({ where: { email } }) |
| 63 | |
| 64 | // Good — raw query with parameters |
| 65 | const users = await this.dataSource.query( |
| 66 | 'SELECT * FROM users WHERE email = $1 AND status = $2', |
| 67 | [email, 'active'] |
| 68 | ) |
| 69 | |
| 70 | // Good — QueryBuilder with parameters |
| 71 | const users = await this.repo |
| 72 | .createQueryBuilder('user') |
| 73 | .where('user.email = :email', { email }) |
| 74 | .andWhere('user.status = :status', { status: 'active' }) |
| 75 | .getMany() |
| 76 | ``` |
| 77 | |
| 78 | ## XSS Prevention |
| 79 | |
| 80 | ```typescript |
| 81 | // Sanitize HTML when you MUST accept user HTML (e.g., rich text editors) |
| 82 | // Install: npm i dompurify jsdom; npm i -D @types/dompurify |
| 83 | |
| 84 | import createDOMPurify from 'dompurify' |
| 85 | import { JSDOM } from 'jsdom' |
| 86 | |
| 87 | const { window } = new JSDOM('') |
| 88 | const DOMPurify = createDOMPurify(window as unknown as Window) |
| 89 | |
| 90 | const allowedTags = ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li'] |
| 91 | const allowedAttrs = { a: ['href', 'title', 'target'] } |
| 92 | |
| 93 | export function sanitizeHtml(dirty: string): string { |
| 94 | return DOMPurify.sanitize(dirty, { |
| 95 | ALLOWED_TAGS: allowedTags, |
| 96 | ALLOWED_ATTR: allowedAttrs, |
| 97 | }) |
| 98 | } |
| 99 | |
| 100 | // In React: NEVER use dangerouslySetInnerHTML with unsanitized content |
| 101 | // Good: |
| 102 | <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(userContent) }} /> |
| 103 | // Bad: |
| 104 | <div dangerouslySetInnerHTML={{ __html: userContent }} /> |
| 105 | ``` |
| 106 | |
| 107 | ## Security Headers |
| 108 | |
| 109 | ```typescript |
| 110 | // main.ts — use helmet |
| 111 | import helmet from 'helmet' |
| 112 | |
| 113 | app.use(helmet({ |
| 114 | contentSecurityPolicy: { |
| 115 | directives: { |
| 116 | defaultSrc: ["'self'"], |
| 117 | scriptSrc: ["'self'", "'nonce-{NONCE}'"], |
| 118 | styleSrc: ["'self'", "'unsafe-inline'"], |
| 119 | imgSrc: ["'self'", 'data:', 'https:'], |
| 120 | connectSrc: ["'self'", 'https://api.example.com'], |
| 121 | }, |
| 122 | }, |
| 123 | hsts: { |
| 124 | maxAge: 31536000, |
| 125 | includeSubDomains: true, |
| 126 | preload: true, |
| 127 | }, |
| 128 | })) |
| 129 | |
| 130 | // CORS — explicit origins only |
| 131 | app.enableCors({ |
| 132 | origin: process.env.CORS_ORIGINS?.split(',') ?? [], |
| 133 | methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'], |
| 134 | credentials: true, |
| 135 | maxAge: 86400, |
| 136 | }) |
| 137 | ``` |
| 138 | |
| 139 | ## Rate Limiting |
| 140 | |
| 141 | ```typescript |
| 142 | // Per-endpoint limits for sensitive operations |
| 143 | @Controller('auth') |
| 144 | export class AuthController { |
| 145 | // Very strict: login attempts |
| 146 | @Post('login') |
| 147 | @Throttle({ default: { ttl: 900_000, limit: 5 } }) // 5 per 15 min |
| 148 | login() {} |
| 149 | |
| 150 | // Strict: password reset |
| 151 | @Post('forgot-password') |
| 152 | @Throttle({ default: { ttl: 3600_000, limit: 3 } }) // 3 per hour |
| 153 | forgotPassword() {} |
| 154 | |
| 155 | // Standard: general API |
| 156 | @Get('profile') |
| 157 | @Throttle({ default: { ttl: 60_000, limit: 60 } }) // 60 per min |
| 158 | profile() {} |
| 159 | } |
| 160 | ``` |
| 161 | |
| 162 | ## CSRF Protection |
| 163 | |
| 164 | ```typescript |
| 165 | // Use csurf for session-based apps |
| 166 | // For JWT-based SPAs: double-submit cookie pattern is unnecessary |
| 167 | // because CORS already protects — just validate the Origin header |
| 168 | |
| 169 | // main.ts — validate origin on state-changing requests |
| 170 | app.use((req, res, ne |