Skip to content

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 &
done

Fix

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

#RiskTeamFlow StatusAction
A01Broken Access Control⚠️ Bug #6Fix RBAC
A02Cryptographic Failures✅ Supabase handles-
A03Injection✅ Parameterized (Supabase)-
A04Insecure Design✅ Auth middleware-
A05Security Misconfig⚠️ CORS openRestrict origins
A06Vulnerable Components⚠️ Check npm auditRun audit fix
A07Auth Failures⚠️ No rate limitFix Bug #11
A08Data Integrity✅ Zod validation-
A09Logging Failures⚠️ MinimalAdd activity log
A10SSRF✅ 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 →

Powered by CodyMaster × VitePress