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
- Có review RLS policies thường xuyên không?
- Secrets management hiện tại như thế nào?
- Đã có security incident nào chưa?