Bỏ qua

Security Considerations

Security Layers

Defense in Depth

┌─────────────────────────────────────────────────────────────┐
│                    SECURITY LAYERS                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Layer 1: NETWORK                                           │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Cloudflare WAF, DDoS Protection, Rate Limiting      │   │
│  └──────────────────────────────────────────────────────┘   │
│                          │                                   │
│  Layer 2: AUTHENTICATION                                    │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Supabase Auth (JWT), API Key Validation             │   │
│  └──────────────────────────────────────────────────────┘   │
│                          │                                   │
│  Layer 3: AUTHORIZATION                                     │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Row Level Security (RLS), Policy Checks             │   │
│  └──────────────────────────────────────────────────────┘   │
│                          │                                   │
│  Layer 4: DATA                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Encryption at Rest, Input Validation, Output        │   │
│  │  Sanitization                                         │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

API Keys Management

Key Types và Usage

┌─────────────────────────────────────────────────────────────┐
│                    API KEY TYPES                             │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  SUPABASE_URL                                               │
│  ├── Type: Public                                           │
│  ├── Expose: YES (client-side OK)                          │
│  └── Usage: Project identification                          │
│                                                              │
│  SUPABASE_ANON_KEY                                          │
│  ├── Type: Public                                           │
│  ├── Expose: YES (client-side OK)                          │
│  ├── Usage: Anonymous/authenticated requests               │
│  └── Note: RLS enforces permissions                         │
│                                                              │
│  SUPABASE_SERVICE_ROLE_KEY                                  │
│  ├── Type: SECRET                                           │
│  ├── Expose: NEVER                                          │
│  ├── Usage: Server-side only, bypasses RLS                 │
│  └── Danger: Full database access!                          │
│                                                              │
│  CLOUDFLARE_API_TOKEN                                       │
│  ├── Type: SECRET                                           │
│  ├── Expose: NEVER                                          │
│  └── Usage: CI/CD deployment only                           │
│                                                              │
│  STRIPE_SECRET_KEY / Third-party API keys                   │
│  ├── Type: SECRET                                           │
│  ├── Expose: NEVER                                          │
│  └── Usage: Server-side only (Workers, Edge Functions)      │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Environment Configuration

// ✅ CORRECT: Next.js environment variables

// .env.local (gitignored)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// Server-only (no NEXT_PUBLIC_ prefix)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!  // ✅ Public key only
  );
}

// lib/supabase/admin.ts (server-only!)
import { createClient } from '@supabase/supabase-js';

export function createAdminClient() {
  // ✅ Only used in server code (API routes, Server Actions)
  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  );
}

RLS Security

RLS as Primary Defense

-- RLS is your MAIN authorization layer
-- Never rely on client-side checks alone

-- Example: Task access control
CREATE POLICY "Users can view own tasks"
  ON tasks FOR SELECT
  USING (user_id = auth.uid());

CREATE POLICY "Users can insert own tasks"
  ON tasks FOR INSERT
  WITH CHECK (user_id = auth.uid());

CREATE POLICY "Users can update own tasks"
  ON tasks FOR UPDATE
  USING (user_id = auth.uid())
  WITH CHECK (user_id = auth.uid());

CREATE POLICY "Users can delete own tasks"
  ON tasks FOR DELETE
  USING (user_id = auth.uid());

Common RLS Mistakes

-- ❌ BAD: Forgot to enable RLS
CREATE TABLE sensitive_data (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  secret TEXT
);
-- Anyone can read/write!

-- ✅ GOOD: Always enable RLS
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;

-- ❌ BAD: Too permissive policy
CREATE POLICY "Anyone can read" ON tasks
  FOR SELECT USING (true);
-- All users see all tasks!

-- ✅ GOOD: Proper scoping
CREATE POLICY "Users read own tasks" ON tasks
  FOR SELECT USING (user_id = auth.uid());

-- ❌ BAD: Missing WITH CHECK on UPDATE
CREATE POLICY "Users update tasks" ON tasks
  FOR UPDATE USING (user_id = auth.uid());
-- User could change user_id to steal task!

-- ✅ GOOD: Both USING and WITH CHECK
CREATE POLICY "Users update own tasks" ON tasks
  FOR UPDATE
  USING (user_id = auth.uid())
  WITH CHECK (user_id = auth.uid());

Worker Security

Request Validation

// workers/api/index.ts
import { Hono } from 'hono';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';

const app = new Hono<{ Bindings: Env }>();

// Input validation schema
const createTaskSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
});

// Validate input
app.post('/api/tasks', zValidator('json', createTaskSchema), async (c) => {
  const data = c.req.valid('json');
  // data is now typed and validated

  const { title, description, priority } = data;
  // Safe to use
});

// Rate limiting
const rateLimiter = new Map<string, number[]>();

app.use('/api/*', async (c, next) => {
  const ip = c.req.header('CF-Connecting-IP') || 'unknown';
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxRequests = 100;

  const requests = rateLimiter.get(ip) || [];
  const recentRequests = requests.filter((t) => t > now - windowMs);

  if (recentRequests.length >= maxRequests) {
    return c.json({ error: 'Too many requests' }, 429);
  }

  recentRequests.push(now);
  rateLimiter.set(ip, recentRequests);

  await next();
});

CORS Configuration

// Proper CORS setup
import { cors } from 'hono/cors';

app.use('/api/*', cors({
  origin: (origin) => {
    // Allow specific origins only
    const allowedOrigins = [
      'https://myapp.com',
      'https://staging.myapp.com',
    ];

    if (process.env.NODE_ENV === 'development') {
      allowedOrigins.push('http://localhost:3000');
    }

    return allowedOrigins.includes(origin) ? origin : null;
  },
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}));

JWT Validation

Verify Supabase JWT in Workers

// workers/api/auth.ts
import { createClient } from '@supabase/supabase-js';

async function validateToken(
  token: string,
  env: Env
): Promise<User | null> {
  const supabase = createClient(
    env.SUPABASE_URL,
    env.SUPABASE_ANON_KEY
  );

  const { data: { user }, error } = await supabase.auth.getUser(token);

  if (error || !user) {
    return null;
  }

  return user;
}

// Middleware
app.use('/api/protected/*', async (c, next) => {
  const authHeader = c.req.header('Authorization');

  if (!authHeader?.startsWith('Bearer ')) {
    return c.json({ error: 'Missing authorization' }, 401);
  }

  const token = authHeader.replace('Bearer ', '');
  const user = await validateToken(token, c.env);

  if (!user) {
    return c.json({ error: 'Invalid token' }, 401);
  }

  c.set('user', user);
  await next();
});

Token Best Practices

// ✅ Always verify on server
// Never trust client-side token claims without verification

// ✅ Check token expiry
const { data: { session } } = await supabase.auth.getSession();
if (!session || new Date(session.expires_at * 1000) < new Date()) {
  // Token expired, refresh or re-authenticate
}

// ✅ Use short-lived tokens
// Supabase access tokens expire in 1 hour by default

// ✅ Refresh tokens properly
const { data, error } = await supabase.auth.refreshSession();

Secrets in Workers

Cloudflare Worker Secrets

# Set secrets via CLI (never commit!)
wrangler secret put SUPABASE_SERVICE_KEY
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put SENDGRID_API_KEY

# Secrets are encrypted and only available at runtime
// Access secrets in Worker
interface Env {
  SUPABASE_URL: string;           // Variable (in wrangler.toml)
  SUPABASE_SERVICE_KEY: string;   // Secret (via CLI)
  STRIPE_SECRET_KEY: string;      // Secret (via CLI)
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // env.SUPABASE_SERVICE_KEY is available here
    // Never log or expose these values!
  },
};

wrangler.toml (Safe Configuration)

# wrangler.toml - committed to git

name = "my-api"
main = "src/index.ts"

[vars]
# Public variables only
SUPABASE_URL = "https://xxx.supabase.co"
APP_ENV = "production"

# NEVER put secrets here!
# SUPABASE_SERVICE_KEY = "..." ❌

Common Vulnerabilities

SQL Injection Prevention

// ❌ BAD: String interpolation (vulnerable!)
const query = `SELECT * FROM tasks WHERE title = '${userInput}'`;

// ✅ GOOD: Parameterized queries (Supabase handles this)
const { data } = await supabase
  .from('tasks')
  .select('*')
  .eq('title', userInput);  // Safe: automatically parameterized

// For raw SQL (RPC functions), use parameters
const { data } = await supabase.rpc('search_tasks', {
  search_term: userInput  // Passed as parameter, not interpolated
});

XSS Prevention

// ✅ React escapes by default
function TaskTitle({ title }: { title: string }) {
  return <h1>{title}</h1>;  // Escaped automatically
}

// ❌ BAD: dangerouslySetInnerHTML with user input
function UnsafeComponent({ html }: { html: string }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// ✅ If HTML is needed, sanitize first
import DOMPurify from 'dompurify';

function SafeHtmlComponent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

CSRF Protection

// Next.js Server Actions have built-in CSRF protection
// For custom APIs, use tokens

// Generate CSRF token
import { randomBytes } from 'crypto';

function generateCsrfToken() {
  return randomBytes(32).toString('hex');
}

// Validate on server
app.post('/api/sensitive', async (c) => {
  const csrfToken = c.req.header('X-CSRF-Token');
  const sessionToken = getCookie(c, 'csrf_token');

  if (!csrfToken || csrfToken !== sessionToken) {
    return c.json({ error: 'CSRF validation failed' }, 403);
  }

  // Proceed with request
});

Security Checklist

Development

✅ RLS enabled on all tables
✅ Policies for SELECT, INSERT, UPDATE, DELETE
✅ Service role key never in client code
✅ Input validation on all endpoints
✅ CORS properly configured

Deployment

✅ Secrets in environment variables only
✅ HTTPS enforced
✅ Rate limiting enabled
✅ Error messages don't leak info
✅ Logs don't contain secrets

Monitoring

✅ Failed auth attempts logged
✅ Unusual patterns detected
✅ Alerts for security events
✅ Regular security audits

Tổng kết

Security Priority

Layer Implementation
Network Cloudflare WAF, rate limiting
Auth Supabase Auth, JWT validation
Authorization RLS policies
Data Input validation, output encoding

Key Rules

1. Never trust client input
2. RLS is mandatory, not optional
3. Service role key = server only
4. Validate everything, sanitize output
5. Log security events, not secrets

Q&A

  1. Có review RLS policies thường xuyên không?
  2. Secrets management hiện tại như thế nào?
  3. Đã có security incident nào chưa?