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()
|
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 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,12 +91,6 @@ export function middleware(request: NextRequest) {
|
||||||
return NextResponse.next()
|
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
|
// Redirect to login
|
||||||
const loginUrl = request.nextUrl.clone()
|
const loginUrl = request.nextUrl.clone()
|
||||||
loginUrl.pathname = '/login'
|
loginUrl.pathname = '/login'
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,27 @@ import { authenticateUser, createSession } from '@/lib/auth'
|
||||||
import { logAuditEvent } from '@/lib/db'
|
import { logAuditEvent } from '@/lib/db'
|
||||||
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
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) {
|
export async function POST(request: Request) {
|
||||||
try {
|
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()
|
const { username, password } = await request.json()
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,20 @@ interface HealthResult {
|
||||||
error?: string
|
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
|
* POST /api/gateways/health - Server-side health probe for all gateways
|
||||||
* Probes gateways from the server where loopback addresses are reachable.
|
* Probes gateways from the server where loopback addresses are reachable.
|
||||||
|
|
@ -33,53 +47,57 @@ export async function POST(request: NextRequest) {
|
||||||
const db = getDatabase()
|
const db = getDatabase()
|
||||||
const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[]
|
const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[]
|
||||||
|
|
||||||
const results: HealthResult[] = await Promise.all(
|
const results: HealthResult[] = []
|
||||||
gateways.map(async (gw) => {
|
|
||||||
const start = Date.now()
|
|
||||||
try {
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
||||||
|
|
||||||
const probeUrl = "http://" + gw.host + ":" + gw.port + "/"
|
for (const gw of gateways) {
|
||||||
const res = await fetch(probeUrl, {
|
const probeUrl = "http://" + gw.host + ":" + gw.port + "/"
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
clearTimeout(timeout)
|
|
||||||
|
|
||||||
const latency = Date.now() - start
|
if (isBlockedUrl(probeUrl)) {
|
||||||
const status = res.ok ? "online" : "error"
|
results.push({ id: gw.id, name: gw.name, status: 'error', latency: null, agents: [], sessions_count: 0, error: 'Blocked URL' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
db.prepare(
|
const start = Date.now()
|
||||||
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
|
try {
|
||||||
).run(status, latency, gw.id)
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 5000)
|
||||||
|
|
||||||
return {
|
const res = await fetch(probeUrl, {
|
||||||
id: gw.id,
|
signal: controller.signal,
|
||||||
name: gw.name,
|
})
|
||||||
status: status as "online" | "error",
|
clearTimeout(timeout)
|
||||||
latency,
|
|
||||||
agents: [],
|
|
||||||
sessions_count: 0,
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
const latency = Date.now() - start
|
|
||||||
|
|
||||||
db.prepare(
|
const latency = Date.now() - start
|
||||||
"UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?"
|
const status = res.ok ? "online" : "error"
|
||||||
).run("offline", gw.id)
|
|
||||||
|
|
||||||
return {
|
db.prepare(
|
||||||
id: gw.id,
|
"UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?"
|
||||||
name: gw.name,
|
).run(status, latency, gw.id)
|
||||||
status: "offline" as const,
|
|
||||||
latency: null,
|
results.push({
|
||||||
agents: [],
|
id: gw.id,
|
||||||
sessions_count: 0,
|
name: gw.name,
|
||||||
error: err.name === "AbortError" ? "timeout" : (err.message || "connection failed"),
|
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() })
|
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() ||
|
String(process.env.MC_DEFAULT_OWNER_GATEWAY || process.env.MC_DEFAULT_GATEWAY_NAME || 'primary').trim() ||
|
||||||
'primary'
|
'primary'
|
||||||
|
|
||||||
db.exec(`
|
db.prepare(`
|
||||||
UPDATE tenants
|
UPDATE tenants
|
||||||
SET owner_gateway = COALESCE(
|
SET owner_gateway = COALESCE(
|
||||||
(SELECT name FROM gateways ORDER BY is_primary DESC, id ASC LIMIT 1),
|
(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) = ''
|
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)`)
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_tenants_owner_gateway ON tenants(owner_gateway)`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue