Security Standards
Security rules and patterns for the Eko codebase. These rules protect against common vulnerabilities and ensure safe handling of user data.
Core Rules
SEC-001: URL Validation (SSRF Prevention)
All external URLs must be validated before fetching to prevent Server-Side Request Forgery (SSRF).
import { verifyAndNormalizeUrl } from '@eko/shared'
// Good: Validate before fetch
async function fetchExternalUrl(url: string) {
const result = await verifyAndNormalizeUrl(url)
if (result.policy_decision !== 'allow') {
throw new Error(`URL blocked: ${result.reason_code}`)
}
return fetch(result.final_url)
}
// Bad: Direct fetch without validation
async function fetchExternalUrl(url: string) {
return fetch(url) // SSRF risk!
}
Blocked URL patterns:
- Internal IPs:
127.*,10.*,192.168.*,172.16-31.*,169.254.* - Localhost:
localhost,0.0.0.0 - Cloud metadata:
169.254.169.254,metadata.google.internal - Non-HTTP protocols:
file://,ftp://,gopher://
Validation function: packages/shared/src/url-verification.ts
SEC-002: Service Role Key Restrictions
The SUPABASE_SERVICE_ROLE_KEY bypasses Row Level Security and must be strictly controlled.
// ALLOWED: Worker processes (server-side only)
// apps/worker-ingest/src/index.ts
import { createServiceClient } from '@eko/db'
const supabase = createServiceClient() // Uses service role
// ALLOWED: Server actions with explicit auth checks
// apps/web/app/api/admin/route.ts
export async function POST(request: Request) {
const user = await requireAuth(request)
if (user.role !== 'admin') {
return errorResponse(403, 'FORBIDDEN', 'Admin access required')
}
// Service role operations...
}
// FORBIDDEN: Client components
// apps/web/components/Dashboard.tsx
const supabase = createServiceClient() // NEVER in client code!
Allowed locations:
apps/worker-*/**— Worker processesapps/web/app/api/**— API routes with auth checksscripts/**— Admin scripts
Forbidden locations:
apps/web/components/**— Client componentsapps/web/app/(dashboard)/**— Client pagespackages/ui/**— UI components
SEC-003: Secrets in Logs
Never log sensitive data that could be exposed in log aggregation.
// Bad: Logging full request with headers
logger.info('API request', { request }) // May contain auth headers!
// Bad: Logging credentials
logger.debug('Auth attempt', { email, password }) // NEVER log passwords!
// Good: Log only safe fields
logger.info('API request', {
method: request.method,
path: request.url,
userId: user?.id,
})
// Good: Redact sensitive data
logger.info('External API call', {
url: externalUrl,
responseStatus: response.status,
// headers intentionally omitted
})
Never log:
- API keys and tokens
- User passwords
- Session tokens
- Full request/response bodies with auth headers
- Credit card numbers
- Personal identification numbers
Safe to log:
- User IDs (not emails unless necessary)
- Request paths and methods
- Response status codes
- Timing information
- Error codes (not full stack traces in production)
SEC-004: RLS Testing
Every table with Row Level Security (RLS) must have corresponding security tests.
// Example RLS test for card_interactions (user-scoped table)
import { describe, expect, it } from 'vitest'
import { createTestClient, seedTestUsers } from '../helpers'
describe('CardInteractions RLS', () => {
const { userA, userB } = seedTestUsers()
describe('SELECT policy', () => {
it('should allow user to read own interactions', async () => {
const client = createTestClient(userA.id)
const { data } = await client
.from('card_interactions')
.select()
.eq('user_id', userA.id)
expect(data).toHaveLength(userA.interactionCount)
})
it('should NOT allow user to read other user interactions', async () => {
const client = createTestClient(userA.id)
const { data } = await client
.from('card_interactions')
.select()
.eq('user_id', userB.id)
expect(data).toHaveLength(0) // RLS blocks access
})
})
describe('INSERT policy', () => {
it('should allow user to insert own interactions', async () => {
// ...
})
it('should NOT allow inserting interactions for other users', async () => {
// ...
})
})
describe('DELETE policy', () => {
it('should allow user to delete own interactions', async () => {
// ...
})
it('should NOT allow deleting other user interactions', async () => {
// ...
})
})
})
Test requirements:
- Test all CRUD operations (SELECT, INSERT, UPDATE, DELETE)
- Test with correct user (should succeed)
- Test with wrong user (should fail/return empty)
- Test without authentication (should fail)
- Test edge cases (soft deletes, shared resources)
Location: packages/db/src/__tests__/security/
SEC-005: Input Validation
All user inputs must be validated with Zod schemas before use.
import { z } from 'zod'
// Define schema
const SeedEntrySchema = z.object({
title: z.string().min(1).max(500),
topicSlug: z.string().min(1),
sourceUrl: z.string().url().max(2048).optional(),
})
// Validate in API route
export async function POST(request: Request) {
const body = await request.json()
const result = SeedEntrySchema.safeParse(body)
if (!result.success) {
return errorResponse(400, 'INVALID_INPUT', result.error.message)
}
const { title, topicSlug, sourceUrl } = result.data
// Now safe to use...
}
Validation points:
- API request bodies
- URL parameters
- Form submissions
- Webhook payloads
- Queue message data
Validation rules:
- Use Zod for all validation
- Define schemas in
packages/shared/src/schemas.ts - Validate early (at API boundary)
- Return 400 with clear error message on failure
- Never trust client-side validation alone
SEC-006: Challenge URL Answer Leak Prevention
Challenge URLs visible in the browser address bar, shared links, or link previews must never contain semantic information that could reveal challenge answers.
Current architecture: Challenges are embedded in card detail pages at /card/[slug]
where the slug is an opaque UUID. Challenge content is loaded server-side for
authenticated users only. This is the safe pattern.
Safe URL patterns:
/card/[uuid]— opaque fact record ID/feed?topic=music— broad category filtering, not answer-specific
Unsafe URL patterns (must never be reintroduced):
/challenges/[format]/[topicSlug]— topic slug reveals the answer subject- Any route with entity names, topic leaf slugs, or answer text as URL segments
- OpenGraph/meta tags that include challenge answers or target fact values
API responses must not include in URLs or query params:
topic.slugortopic.pathwhen they identify challenge targets- Entity names that are challenge answers
correctAnswervalues in any URL-accessible location
Enforcement: INV-008 invariant check
Security Checklist for New Features
When adding new features, check:
- Authentication: Does this endpoint require auth?
- Authorization: Does the user have permission for this action?
- Input validation: Are all inputs validated with Zod?
- SQL injection: Using parameterized queries (Drizzle/Supabase)?
- XSS: User content properly escaped in UI?
- SSRF: External URLs validated before fetch?
- RLS: New tables have appropriate policies?
- Secrets: No sensitive data logged?
- Challenge URLs: No answer data leaked in URLs or metadata?
- Rate limiting: Endpoint protected against abuse?
Secret Scanning
The repository uses gitleaks for pre-commit secret scanning.
# Manual scan
gitleaks detect --config .gitleaks.toml
# Scan staged files
gitleaks protect --staged --config .gitleaks.toml
Configuration: .gitleaks.toml
Detected secrets:
- Supabase keys (service role, anon)
- Upstash Redis tokens
- OpenAI/Anthropic API keys
- Stripe API keys
- Resend API keys
False positive handling:
- Add pattern to
.gitleaks.tomlallowlist - Use
gitleaks:allowcomment on the line
Related
- SECURITY.md — Vulnerability reporting policy
- AI Safety Policy — LLM safety guidelines
- Rules Index — All rules