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:
M1
2026-03-17 06:10:10 +04:00
parent 5071e340c7
commit 6bdd76b4f0
10 changed files with 250 additions and 72 deletions
+15 -10
View File
@@ -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 } });