fix: add auth checks on all GET endpoints, timing-safe comparisons, and XSS sanitization
This commit is contained in:
parent
84ba833454
commit
1ee506b4cf
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -211,6 +211,9 @@ 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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 resolvedParams = await params;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,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;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ function hasAegisApproval(db: ReturnType<typeof getDatabase>, 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);
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -321,8 +321,13 @@ export function MemoryBrowserPanel() {
|
|||
elements.push(<div key={`${i}-space`} className="mb-2"></div>)
|
||||
} 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, '"')
|
||||
.replace(/'/g, ''')
|
||||
// Simple bold formatting
|
||||
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
// Simple italic formatting
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue