From 1ee506b4cf7d96caabe5e2de014318a57ff35681 Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Fri, 27 Feb 2026 13:04:24 +0700 Subject: [PATCH] fix: add auth checks on all GET endpoints, timing-safe comparisons, and XSS sanitization --- middleware.ts | 20 ++++++++++++++++--- src/app/api/activities/route.ts | 4 ++++ src/app/api/agents/[id]/heartbeat/route.ts | 3 +++ src/app/api/agents/[id]/route.ts | 3 +++ src/app/api/agents/[id]/soul/route.ts | 3 +++ src/app/api/agents/comms/route.ts | 4 ++++ src/app/api/agents/route.ts | 3 +++ src/app/api/auth/access-requests/route.ts | 5 ++++- src/app/api/auth/me/route.ts | 5 ++++- src/app/api/auth/users/route.ts | 5 ++++- src/app/api/chat/conversations/route.ts | 4 ++++ src/app/api/chat/messages/[id]/route.ts | 3 +++ src/app/api/chat/messages/route.ts | 3 +++ src/app/api/cron/route.ts | 3 +++ src/app/api/events/route.ts | 7 ++++++- src/app/api/logs/route.ts | 3 +++ src/app/api/notifications/deliver/route.ts | 3 +++ src/app/api/notifications/route.ts | 3 +++ src/app/api/pipelines/route.ts | 5 ++++- src/app/api/pipelines/run/route.ts | 3 +++ src/app/api/quality-review/route.ts | 3 +++ src/app/api/sessions/route.ts | 8 ++++++-- src/app/api/spawn/route.ts | 3 +++ src/app/api/standup/route.ts | 5 ++++- src/app/api/status/route.ts | 4 ++++ src/app/api/tasks/[id]/comments/route.ts | 5 ++++- src/app/api/tasks/[id]/route.ts | 5 ++++- src/app/api/tasks/route.ts | 5 ++++- src/app/api/tokens/route.ts | 3 +++ src/app/api/workflows/route.ts | 5 ++++- .../panels/memory-browser-panel.tsx | 9 +++++++-- src/lib/auth.ts | 18 +++++++++++++++-- 32 files changed, 146 insertions(+), 19 deletions(-) diff --git a/middleware.ts b/middleware.ts index f30dffd..1b551c0 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,6 +1,20 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' +/** Edge-compatible constant-time string comparison. */ +function safeCompare(a: string, b: string): boolean { + if (typeof a !== 'string' || typeof b !== 'string') return false + const encoder = new TextEncoder() + const bufA = encoder.encode(a) + const bufB = encoder.encode(b) + if (bufA.length !== bufB.length) return false + let result = 0 + for (let i = 0; i < bufA.length; i++) { + result |= bufA[i] ^ bufB[i] + } + return result === 0 +} + function envFlag(name: string): boolean { const raw = process.env[name] if (raw === undefined) return false @@ -65,13 +79,13 @@ export function middleware(request: NextRequest) { // API routes: accept session cookie OR API key if (pathname.startsWith('/api/')) { const apiKey = request.headers.get('x-api-key') - if (sessionToken || (apiKey && apiKey === process.env.API_KEY)) { + if (sessionToken || (apiKey && safeCompare(apiKey, process.env.API_KEY || ''))) { return NextResponse.next() } // Backward compat: accept legacy cookie during migration const legacyCookie = request.cookies.get('mission-control-auth') - if (legacyCookie?.value === process.env.AUTH_SECRET) { + if (safeCompare(legacyCookie?.value || '', process.env.AUTH_SECRET || '')) { return NextResponse.next() } @@ -85,7 +99,7 @@ export function middleware(request: NextRequest) { // Backward compat: accept legacy cookie const legacyCookie = request.cookies.get('mission-control-auth') - if (legacyCookie?.value === process.env.AUTH_SECRET) { + if (safeCompare(legacyCookie?.value || '', process.env.AUTH_SECRET || '')) { return NextResponse.next() } diff --git a/src/app/api/activities/route.ts b/src/app/api/activities/route.ts index 378c814..f16b25d 100644 --- a/src/app/api/activities/route.ts +++ b/src/app/api/activities/route.ts @@ -1,11 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { getDatabase, Activity } from '@/lib/db'; +import { requireRole } from '@/lib/auth' /** * GET /api/activities - Get activity stream or stats * Query params: type, actor, entity_type, limit, offset, since, hours (for stats) */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const { searchParams, pathname } = new URL(request.url); diff --git a/src/app/api/agents/[id]/heartbeat/route.ts b/src/app/api/agents/[id]/heartbeat/route.ts index 4dc120f..4d40b35 100644 --- a/src/app/api/agents/[id]/heartbeat/route.ts +++ b/src/app/api/agents/[id]/heartbeat/route.ts @@ -16,6 +16,9 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase(); const resolvedParams = await params; diff --git a/src/app/api/agents/[id]/route.ts b/src/app/api/agents/[id]/route.ts index ee80987..e4acd52 100644 --- a/src/app/api/agents/[id]/route.ts +++ b/src/app/api/agents/[id]/route.ts @@ -11,6 +11,9 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { id } = await params diff --git a/src/app/api/agents/[id]/soul/route.ts b/src/app/api/agents/[id]/soul/route.ts index 2d84f67..df097ad 100644 --- a/src/app/api/agents/[id]/soul/route.ts +++ b/src/app/api/agents/[id]/soul/route.ts @@ -13,6 +13,9 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase(); const resolvedParams = await params; diff --git a/src/app/api/agents/comms/route.ts b/src/app/api/agents/comms/route.ts index 29b0c01..42e62df 100644 --- a/src/app/api/agents/comms/route.ts +++ b/src/app/api/agents/comms/route.ts @@ -1,11 +1,15 @@ import { NextRequest, NextResponse } from "next/server" import { getDatabase, Message } from "@/lib/db" +import { requireRole } from '@/lib/auth' /** * GET /api/agents/comms - Inter-agent communication stats and timeline * Query params: limit, offset, since, agent */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { searchParams } = new URL(request.url) diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index a2d24a8..f21017c 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -11,6 +11,9 @@ import { getUserFromRequest, requireRole } from '@/lib/auth'; * Query params: status, role, limit, offset */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase(); const { searchParams } = new URL(request.url); diff --git a/src/app/api/auth/access-requests/route.ts b/src/app/api/auth/access-requests/route.ts index 51b7fa8..83b3195 100644 --- a/src/app/api/auth/access-requests/route.ts +++ b/src/app/api/auth/access-requests/route.ts @@ -1,6 +1,6 @@ import { randomBytes } from 'crypto' import { NextRequest, NextResponse } from 'next/server' -import { createUser, getUserFromRequest } from '@/lib/auth' +import { createUser, getUserFromRequest , requireRole } from '@/lib/auth' import { getDatabase, logAuditEvent } from '@/lib/db' function makeUsernameFromEmail(email: string): string { @@ -20,6 +20,9 @@ function ensureUniqueUsername(base: string): string { } export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const user = getUserFromRequest(request) if (!user || user.role !== 'admin') { return NextResponse.json({ error: 'Admin access required' }, { status: 403 }) diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts index 8a4f0d8..3078cf9 100644 --- a/src/app/api/auth/me/route.ts +++ b/src/app/api/auth/me/route.ts @@ -1,9 +1,12 @@ import { NextRequest, NextResponse } from 'next/server' -import { getUserFromRequest, updateUser } from '@/lib/auth' +import { getUserFromRequest, updateUser , requireRole } from '@/lib/auth' import { logAuditEvent } from '@/lib/db' import { verifyPassword } from '@/lib/password' export async function GET(request: Request) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const user = getUserFromRequest(request) if (!user) { diff --git a/src/app/api/auth/users/route.ts b/src/app/api/auth/users/route.ts index 18bed85..3f7bbaa 100644 --- a/src/app/api/auth/users/route.ts +++ b/src/app/api/auth/users/route.ts @@ -1,11 +1,14 @@ import { NextRequest, NextResponse } from 'next/server' -import { getUserFromRequest, getAllUsers, createUser, updateUser, deleteUser } from '@/lib/auth' +import { getUserFromRequest, getAllUsers, createUser, updateUser, deleteUser , requireRole } from '@/lib/auth' import { logAuditEvent } from '@/lib/db' /** * GET /api/auth/users - List all users (admin only) */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const user = getUserFromRequest(request) if (!user || user.role !== 'admin') { return NextResponse.json({ error: 'Admin access required' }, { status: 403 }) diff --git a/src/app/api/chat/conversations/route.ts b/src/app/api/chat/conversations/route.ts index 1b32f09..b2b7d7c 100644 --- a/src/app/api/chat/conversations/route.ts +++ b/src/app/api/chat/conversations/route.ts @@ -1,11 +1,15 @@ import { NextRequest, NextResponse } from 'next/server' import { getDatabase } from '@/lib/db' +import { requireRole } from '@/lib/auth' /** * GET /api/chat/conversations - List conversations derived from messages * Query params: agent (filter by participant), limit, offset */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { searchParams } = new URL(request.url) diff --git a/src/app/api/chat/messages/[id]/route.ts b/src/app/api/chat/messages/[id]/route.ts index 0995d44..26e9c75 100644 --- a/src/app/api/chat/messages/[id]/route.ts +++ b/src/app/api/chat/messages/[id]/route.ts @@ -9,6 +9,9 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { id } = await params diff --git a/src/app/api/chat/messages/route.ts b/src/app/api/chat/messages/route.ts index 78991cb..ae9d936 100644 --- a/src/app/api/chat/messages/route.ts +++ b/src/app/api/chat/messages/route.ts @@ -96,6 +96,9 @@ function extractReplyText(waitPayload: any): string | null { * Query params: conversation_id, from_agent, to_agent, limit, offset, since */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { searchParams } = new URL(request.url) diff --git a/src/app/api/cron/route.ts b/src/app/api/cron/route.ts index d316d14..7c2b068 100644 --- a/src/app/api/cron/route.ts +++ b/src/app/api/cron/route.ts @@ -131,6 +131,9 @@ function mapOpenClawJob(job: OpenClawCronJob): CronJob { } export async function GET(request: NextRequest) { + const auth = requireRole(request, 'admin') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const { searchParams } = new URL(request.url) const action = searchParams.get('action') diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index 6b1f0ec..d25b8c8 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -1,4 +1,6 @@ +import { NextRequest , NextResponse } from 'next/server' import { eventBus, ServerEvent } from '@/lib/event-bus' +import { requireRole } from '@/lib/auth' export const dynamic = 'force-dynamic' export const runtime = 'nodejs' @@ -7,7 +9,10 @@ export const runtime = 'nodejs' * GET /api/events - Server-Sent Events stream for real-time DB mutations. * Clients connect via EventSource and receive JSON-encoded events. */ -export async function GET() { +export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + const encoder = new TextEncoder() // Cleanup function, set in start(), called in cancel() diff --git a/src/app/api/logs/route.ts b/src/app/api/logs/route.ts index c7008ff..b964a8b 100644 --- a/src/app/api/logs/route.ts +++ b/src/app/api/logs/route.ts @@ -175,6 +175,9 @@ async function readLogFile(filePath: string, source: string, maxLines: number): } export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const { searchParams } = new URL(request.url) const action = searchParams.get('action') || 'recent' diff --git a/src/app/api/notifications/deliver/route.ts b/src/app/api/notifications/deliver/route.ts index cb0949a..8281b90 100644 --- a/src/app/api/notifications/deliver/route.ts +++ b/src/app/api/notifications/deliver/route.ts @@ -182,6 +182,9 @@ export async function POST(request: NextRequest) { * GET /api/notifications/deliver - Get delivery status and statistics */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase(); const { searchParams } = new URL(request.url); diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts index 41bbbb0..35b3f40 100644 --- a/src/app/api/notifications/route.ts +++ b/src/app/api/notifications/route.ts @@ -7,6 +7,9 @@ import { requireRole } from '@/lib/auth'; * Query params: recipient, unread_only, type, limit, offset */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase(); const { searchParams } = new URL(request.url); diff --git a/src/app/api/pipelines/route.ts b/src/app/api/pipelines/route.ts index c63fe73..d68f53a 100644 --- a/src/app/api/pipelines/route.ts +++ b/src/app/api/pipelines/route.ts @@ -23,7 +23,10 @@ export interface Pipeline { /** * GET /api/pipelines - List all pipelines with enriched step data */ -export async function GET() { +export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const pipelines = db.prepare( diff --git a/src/app/api/pipelines/run/route.ts b/src/app/api/pipelines/run/route.ts index f0f7cc8..5ded9c8 100644 --- a/src/app/api/pipelines/run/route.ts +++ b/src/app/api/pipelines/run/route.ts @@ -35,6 +35,9 @@ interface PipelineRun { * GET /api/pipelines/run - Get pipeline runs */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { searchParams } = new URL(request.url) diff --git a/src/app/api/quality-review/route.ts b/src/app/api/quality-review/route.ts index 1d18801..e558230 100644 --- a/src/app/api/quality-review/route.ts +++ b/src/app/api/quality-review/route.ts @@ -3,6 +3,9 @@ import { getDatabase, db_helpers } from '@/lib/db' import { requireRole } from '@/lib/auth' export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const { searchParams } = new URL(request.url) diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts index 05414d4..64e8177 100644 --- a/src/app/api/sessions/route.ts +++ b/src/app/api/sessions/route.ts @@ -1,7 +1,11 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { getAllGatewaySessions } from '@/lib/sessions' +import { requireRole } from '@/lib/auth' + +export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) -export async function GET() { try { const gatewaySessions = getAllGatewaySessions() diff --git a/src/app/api/spawn/route.ts b/src/app/api/spawn/route.ts index d32b9c6..db5b5c5 100644 --- a/src/app/api/spawn/route.ts +++ b/src/app/api/spawn/route.ts @@ -99,6 +99,9 @@ export async function POST(request: NextRequest) { // Get spawn history export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const { searchParams } = new URL(request.url) const limit = parseInt(searchParams.get('limit') || '50') diff --git a/src/app/api/standup/route.ts b/src/app/api/standup/route.ts index 2bdf236..a96bec9 100644 --- a/src/app/api/standup/route.ts +++ b/src/app/api/standup/route.ts @@ -211,10 +211,13 @@ export async function POST(request: NextRequest) { * Query params: limit, offset */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer'); + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + try { const db = getDatabase(); const { searchParams } = new URL(request.url); - + const limit = parseInt(searchParams.get('limit') || '10'); const offset = parseInt(searchParams.get('offset') || '0'); diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index dba40ca..b540f5f 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -5,8 +5,12 @@ import { runCommand, runOpenClaw, runClawdbot } from '@/lib/command' import { config } from '@/lib/config' import { getDatabase } from '@/lib/db' import { getAllGatewaySessions, getAgentLiveStatuses } from '@/lib/sessions' +import { requireRole } from '@/lib/auth' export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const { searchParams } = new URL(request.url) const action = searchParams.get('action') || 'overview' diff --git a/src/app/api/tasks/[id]/comments/route.ts b/src/app/api/tasks/[id]/comments/route.ts index 32de528..a0a8c6a 100644 --- a/src/app/api/tasks/[id]/comments/route.ts +++ b/src/app/api/tasks/[id]/comments/route.ts @@ -9,11 +9,14 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = requireRole(request, 'viewer'); + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + try { const db = getDatabase(); const resolvedParams = await params; const taskId = parseInt(resolvedParams.id); - + if (isNaN(taskId)) { return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 }); } diff --git a/src/app/api/tasks/[id]/route.ts b/src/app/api/tasks/[id]/route.ts index 23b0ada..2e5e14d 100644 --- a/src/app/api/tasks/[id]/route.ts +++ b/src/app/api/tasks/[id]/route.ts @@ -20,11 +20,14 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const auth = requireRole(request, 'viewer'); + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + try { const db = getDatabase(); const resolvedParams = await params; const taskId = parseInt(resolvedParams.id); - + if (isNaN(taskId)) { return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 }); } diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 40567c5..9b5a43c 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -18,10 +18,13 @@ function hasAegisApproval(db: ReturnType, taskId: number): b * Query params: status, assigned_to, priority, limit, offset */ export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer'); + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }); + try { const db = getDatabase(); const { searchParams } = new URL(request.url); - + // Parse query parameters const status = searchParams.get('status'); const assigned_to = searchParams.get('assigned_to'); diff --git a/src/app/api/tokens/route.ts b/src/app/api/tokens/route.ts index 06bde54..0b9f241 100644 --- a/src/app/api/tokens/route.ts +++ b/src/app/api/tokens/route.ts @@ -170,6 +170,9 @@ function filterByTimeframe(records: TokenUsageRecord[], timeframe: string): Toke } export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const { searchParams } = new URL(request.url) const action = searchParams.get('action') || 'list' diff --git a/src/app/api/workflows/route.ts b/src/app/api/workflows/route.ts index d4f5ee5..dac20d6 100644 --- a/src/app/api/workflows/route.ts +++ b/src/app/api/workflows/route.ts @@ -21,7 +21,10 @@ export interface WorkflowTemplate { /** * GET /api/workflows - List all workflow templates */ -export async function GET() { +export async function GET(request: NextRequest) { + const auth = requireRole(request, 'viewer') + if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) + try { const db = getDatabase() const templates = db.prepare('SELECT * FROM workflow_templates ORDER BY use_count DESC, updated_at DESC').all() as WorkflowTemplate[] diff --git a/src/components/panels/memory-browser-panel.tsx b/src/components/panels/memory-browser-panel.tsx index ba1dc0b..e40fdb7 100644 --- a/src/components/panels/memory-browser-panel.tsx +++ b/src/components/panels/memory-browser-panel.tsx @@ -321,11 +321,16 @@ export function MemoryBrowserPanel() { elements.push(
) } else if (trimmedLine.length > 0) { if (inList) inList = false - // Handle inline formatting + // Handle inline formatting — escape HTML entities first to prevent XSS let content = trimmedLine + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') // Simple bold formatting content = content.replace(/\*\*(.*?)\*\*/g, '$1') - // Simple italic formatting + // Simple italic formatting content = content.replace(/\*(.*?)\*/g, '$1') elements.push( diff --git a/src/lib/auth.ts b/src/lib/auth.ts index fb9da6b..4bda684 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,7 +1,21 @@ -import { randomBytes } from 'crypto' +import { randomBytes, timingSafeEqual } from 'crypto' import { getDatabase } from './db' import { hashPassword, verifyPassword } from './password' +/** + * Constant-time string comparison to prevent timing attacks. + */ +export function safeCompare(a: string, b: string): boolean { + if (typeof a !== 'string' || typeof b !== 'string') return false + const bufA = Buffer.from(a) + const bufB = Buffer.from(b) + if (bufA.length !== bufB.length) { + timingSafeEqual(bufA, bufA) + return false + } + return timingSafeEqual(bufA, bufB) +} + export interface User { id: number username: string @@ -202,7 +216,7 @@ export function getUserFromRequest(request: Request): User | null { // Check API key - return synthetic user const apiKey = request.headers.get('x-api-key') - if (apiKey && apiKey === process.env.API_KEY) { + if (apiKey && safeCompare(apiKey, process.env.API_KEY || '')) { return { id: 0, username: 'api',