fix: remove legacy auth, add login rate limiting, block SSRF metadata, parameterize migration SQL
This commit is contained in:
parent
98f1990b57
commit
3b600d817e
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue