fix: remove legacy auth, add login rate limiting, block SSRF metadata, parameterize migration SQL

This commit is contained in:
Nyk 2026-02-27 12:50:58 +07:00
parent 98f1990b57
commit 3b600d817e
4 changed files with 81 additions and 56 deletions

View File

@ -83,12 +83,6 @@ export function middleware(request: NextRequest) {
return NextResponse.next()
}
// Backward compat: accept legacy cookie during migration
const legacyCookie = request.cookies.get('mission-control-auth')
if (safeCompare(legacyCookie?.value || '', process.env.AUTH_SECRET || '')) {
return NextResponse.next()
}
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
@ -97,12 +91,6 @@ export function middleware(request: NextRequest) {
return NextResponse.next()
}
// Backward compat: accept legacy cookie
const legacyCookie = request.cookies.get('mission-control-auth')
if (safeCompare(legacyCookie?.value || '', process.env.AUTH_SECRET || '')) {
return NextResponse.next()
}
// Redirect to login
const loginUrl = request.nextUrl.clone()
loginUrl.pathname = '/login'

View File

@ -3,8 +3,27 @@ import { authenticateUser, createSession } from '@/lib/auth'
import { logAuditEvent } from '@/lib/db'
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
// Rate limiting: 5 attempts per minute per IP
const loginAttempts = new Map<string, { count: number; resetAt: number }>()
function checkRateLimit(ip: string): boolean {
const now = Date.now()
const entry = loginAttempts.get(ip)
if (!entry || now > entry.resetAt) {
loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 })
return true
}
entry.count++
return entry.count <= 5
}
export async function POST(request: Request) {
try {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
if (!checkRateLimit(ip)) {
return NextResponse.json({ error: 'Too many login attempts. Try again in a minute.' }, { status: 429 })
}
const { username, password } = await request.json()
if (!username || !password) {

View File

@ -22,6 +22,20 @@ interface HealthResult {
error?: string
}
function isBlockedUrl(urlStr: string): boolean {
try {
const url = new URL(urlStr)
const hostname = url.hostname
// Block link-local / cloud metadata endpoints
if (hostname.startsWith('169.254.')) return true
// Block well-known cloud metadata hostnames
if (hostname === 'metadata.google.internal') return true
return false
} catch {
return true // Block malformed URLs
}
}
/**
* POST /api/gateways/health - Server-side health probe for all gateways
* Probes gateways from the server where loopback addresses are reachable.
@ -33,53 +47,57 @@ export async function POST(request: NextRequest) {
const db = getDatabase()
const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[]
const results: HealthResult[] = await Promise.all(
gateways.map(async (gw) => {
const start = Date.now()
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
const results: HealthResult[] = []
const probeUrl = "http://" + gw.host + ":" + gw.port + "/"
const res = await fetch(probeUrl, {
signal: controller.signal,
})
clearTimeout(timeout)
for (const gw of gateways) {
const probeUrl = "http://" + gw.host + ":" + gw.port + "/"
const latency = Date.now() - start
const status = res.ok ? "online" : "error"
if (isBlockedUrl(probeUrl)) {
results.push({ id: gw.id, name: gw.name, status: 'error', latency: null, agents: [], sessions_count: 0, error: 'Blocked URL' })
continue
}
db.prepare(
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
).run(status, latency, gw.id)
const start = Date.now()
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
return {
id: gw.id,
name: gw.name,
status: status as "online" | "error",
latency,
agents: [],
sessions_count: 0,
}
} catch (err: any) {
const latency = Date.now() - start
const res = await fetch(probeUrl, {
signal: controller.signal,
})
clearTimeout(timeout)
db.prepare(
"UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?"
).run("offline", gw.id)
const latency = Date.now() - start
const status = res.ok ? "online" : "error"
return {
id: gw.id,
name: gw.name,
status: "offline" as const,
latency: null,
agents: [],
sessions_count: 0,
error: err.name === "AbortError" ? "timeout" : (err.message || "connection failed"),
}
}
})
)
db.prepare(
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
).run(status, latency, gw.id)
results.push({
id: gw.id,
name: gw.name,
status: status as "online" | "error",
latency,
agents: [],
sessions_count: 0,
})
} catch (err: any) {
db.prepare(
"UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?"
).run("offline", gw.id)
results.push({
id: gw.id,
name: gw.name,
status: "offline" as const,
latency: null,
agents: [],
sessions_count: 0,
error: err.name === "AbortError" ? "timeout" : (err.message || "connection failed"),
})
}
}
return NextResponse.json({ results, probed_at: Date.now() })
}

View File

@ -348,14 +348,14 @@ const migrations: Migration[] = [
String(process.env.MC_DEFAULT_OWNER_GATEWAY || process.env.MC_DEFAULT_GATEWAY_NAME || 'primary').trim() ||
'primary'
db.exec(`
db.prepare(`
UPDATE tenants
SET owner_gateway = COALESCE(
(SELECT name FROM gateways ORDER BY is_primary DESC, id ASC LIMIT 1),
'${defaultGatewayName.replace(/'/g, "''")}'
?
)
WHERE owner_gateway IS NULL OR trim(owner_gateway) = ''
`)
`).run(defaultGatewayName)
db.exec(`CREATE INDEX IF NOT EXISTS idx_tenants_owner_gateway ON tenants(owner_gateway)`)
}