security: auth redesign, SSRF protection, CORS lockdown, and 13 other fixes
- Auth (#2/#3): UUID PK, 256-bit keys, SHA-256 lookup + bcrypt hash - SSRF (#1): validate URLs, block private IPs, cloud metadata endpoints - CORS (#4): lock to pingql.com origins, not wildcard - SSE limit (#6): 10 connections per monitor max - ReDoS (#7): cap $regex patterns at 200 chars - Monitor limit (#8): 100 per account default - Cookie env config (#9): secure/domain from env vars - Bearer parsing (#10): case-insensitive RFC 6750 - Pings retention (#11): 90-day pruner, hourly interval - monitors.enabled index (#12): partial index for /internal/due - Runner locking (#14): locked_until for horizontal scale safety - COALESCE nullable bug (#17): dynamic PATCH with explicit undefined checks - MONITOR_TOKEN null guard (#18): startup validation + middleware hardening - reset-key cookie fix (#16): sets new cookie in response
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import sql from "../db";
|
||||
import { resolveKey } from "./auth";
|
||||
|
||||
// ── SSE bus ───────────────────────────────────────────────────────────────────
|
||||
type SSEController = ReadableStreamDefaultController<Uint8Array>;
|
||||
@@ -84,25 +85,29 @@ export const ingest = new Elysia()
|
||||
detail: { hide: true },
|
||||
})
|
||||
|
||||
// SSE: stream live pings — auth via Bearer header
|
||||
// SSE: stream live pings — auth via Bearer header or cookie
|
||||
.get("/monitors/:id/stream", async ({ params, headers, cookie, error }) => {
|
||||
const key = headers["authorization"]?.replace("Bearer ", "").trim()
|
||||
?? cookie?.pingql_key?.value;
|
||||
// Case-insensitive bearer parsing
|
||||
const authHeader = headers["authorization"] ?? "";
|
||||
const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim();
|
||||
const key = bearer ?? cookie?.pingql_key?.value;
|
||||
|
||||
if (!key) return error(401, { error: "Unauthorized" });
|
||||
|
||||
// Resolve account from primary key or sub-key
|
||||
const [acc] = await sql`SELECT id FROM accounts WHERE id = ${key}`;
|
||||
const accountId = acc?.id ?? (
|
||||
await sql`SELECT account_id FROM api_keys WHERE id = ${key}`.then(r => r[0]?.account_id)
|
||||
);
|
||||
if (!accountId) return error(401, { error: "Unauthorized" });
|
||||
const resolved = await resolveKey(key);
|
||||
if (!resolved) return error(401, { error: "Unauthorized" });
|
||||
|
||||
// Verify ownership
|
||||
const [monitor] = await sql`
|
||||
SELECT id FROM monitors WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
SELECT id FROM monitors WHERE id = ${params.id} AND account_id = ${resolved.accountId}
|
||||
`;
|
||||
if (!monitor) return error(404, { error: "Not found" });
|
||||
|
||||
// SSE connection limit per monitor
|
||||
const limit = Number(process.env.MAX_SSE_PER_MONITOR ?? 10);
|
||||
if ((bus.get(params.id)?.size ?? 0) >= limit) {
|
||||
return error(429, { error: "Too many connections for this monitor" });
|
||||
}
|
||||
|
||||
return makeSSEStream(params.id);
|
||||
}, { detail: { hide: true } });
|
||||
|
||||
Reference in New Issue
Block a user