Chương 16: Security Audit — Trước Khi Mở Cửa

"Hà: 'Trước khi deploy, tôi cần chắc chắn: không có secret leak, không có RBAC bypass, không có rate limit bypass.' Đức: 'Nghiêm túc vậy?' Hà: 'Một lỗ hổng = mất hết dữ liệu khách hàng.'"
🎯 Mục tiêu
- RBAC (Role-Based Access Control)
- Rate limiting
- Secret scanning
- OWASP checklist
- 🐛 Bug #6: Missing RBAC, Bug #11: No rate limiting
Phần 1: RBAC (25 phút)
Bug #6: Missing RBAC
typescript
// 🐛 BUG: Any authenticated user can delete ANY project
router.delete('/:id', requireAuth, async (req, res) => {
// No check: is this user the OWNER of this project?
await supabase.from('projects').delete().eq('id', req.params.id)
res.json({ message: 'Deleted' })
})Fix: Role-Based Middleware
typescript
// src/middleware/rbac.ts
export function requireRole(...roles: string[]) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = (req as any).user
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single()
if (!profile || !roles.includes(profile.role)) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
next()
}
}
export function requireOwnership(table: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const user = (req as any).user
const { data } = await supabase
.from(table)
.select('owner_id')
.eq('id', req.params.id)
.single()
if (!data || data.owner_id !== user.id) {
return res.status(403).json({ error: 'Not the owner' })
}
next()
}
}
// ✅ Fixed route
router.delete('/:id', requireAuth, requireOwnership('projects'), async (req, res) => {
await supabase.from('projects').delete().eq('id', req.params.id)
res.json({ message: 'Deleted' })
})Phần 2: Rate Limiting (20 phút)
Bug #11: No Rate Limiting
bash
# Attack: 1000 requests/second → server overload
for i in {1..1000}; do
curl http://localhost:3000/api/projects &
doneFix
typescript
// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit'
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests. Try again later.' },
standardHeaders: true
})
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // Only 10 login attempts per 15 min
message: { error: 'Too many login attempts.' }
})
// Apply
app.use('/api/', apiLimiter)
app.use('/api/auth/login', authLimiter)Phần 3: Security Scan (25 phút)
bash
# Secret scanning
antigravity "@cm-secret-shield Full security scan:
1. Scan for hardcoded secrets
2. Check .env.example exists
3. Verify .gitignore covers secrets
4. Check npm audit
5. Scan for SQL injection patterns
6. Check CORS configuration"OWASP Top 10 Checklist
| # | Risk | TeamFlow Status | Action |
|---|---|---|---|
| A01 | Broken Access Control | ⚠️ Bug #6 | Fix RBAC |
| A02 | Cryptographic Failures | ✅ Supabase handles | - |
| A03 | Injection | ✅ Parameterized (Supabase) | - |
| A04 | Insecure Design | ✅ Auth middleware | - |
| A05 | Security Misconfig | ⚠️ CORS open | Restrict origins |
| A06 | Vulnerable Components | ⚠️ Check npm audit | Run audit fix |
| A07 | Auth Failures | ⚠️ No rate limit | Fix Bug #11 |
| A08 | Data Integrity | ✅ Zod validation | - |
| A09 | Logging Failures | ⚠️ Minimal | Add activity log |
| A10 | SSRF | ✅ No external fetches | - |
Phần 4: Security Headers (15 phút)
typescript
import helmet from 'helmet'
app.use(helmet())
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "cdn.jsdelivr.net"],
styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com"],
fontSrc: ["fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "*.supabase.co"],
connectSrc: ["'self'", "*.supabase.co"]
}
}))Homework
- [ ] RBAC implemented & tested
- [ ] Rate limiting active
- [ ] Security scan clean
- [ ] OWASP checklist complete
Chương tiếp: CI/CD — Từ Code Đến Cloudflare →