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
+6 -1
View File
@@ -230,6 +230,7 @@ function evalOp(op: string, fieldVal: unknown, opVal: unknown): boolean {
return typeof fieldVal === "string" && typeof opVal === "string" && fieldVal.endsWith(opVal);
case "$regex": {
if (typeof fieldVal !== "string" || typeof opVal !== "string") return false;
if (opVal.length > 200) return false;
try {
return new RegExp(opVal).test(fieldVal);
} catch {
@@ -311,11 +312,15 @@ export function validateQuery(query: unknown, path = ""): ValidationError[] {
errors.push({ path: keyPath, message: `Unknown field: ${key}. Use status, body, or headers.*` });
}
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
for (const op of Object.keys(value as Record<string, unknown>)) {
const ops = value as Record<string, unknown>;
for (const op of Object.keys(ops)) {
if (!op.startsWith("$")) continue;
if (!VALID_OPS.has(op)) {
errors.push({ path: `${keyPath}.${op}`, message: `Unknown operator: ${op}` });
}
if (op === "$regex" && typeof ops[op] === "string" && (ops[op] as string).length > 200) {
errors.push({ path: `${keyPath}.${op}`, message: "Regex pattern too long (max 200 characters)" });
}
}
}
}