fix: harden auth, SSRF, query engine, and cookie security
This commit is contained in:
@@ -1,13 +1,36 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { createHash } from "crypto";
|
||||
import { createHmac, randomBytes } from "crypto";
|
||||
import sql from "../db";
|
||||
|
||||
const EMAIL_HMAC_KEY = process.env.EMAIL_HMAC_KEY || "pingql-default-hmac-key";
|
||||
|
||||
// ── Per-IP rate limiting for auth endpoints ───────────────────────────
|
||||
const authRateMap = new Map<string, { count: number; resetAt: number }>();
|
||||
|
||||
function checkAuthRateLimit(ip: string, maxPerMinute: number): boolean {
|
||||
const now = Date.now();
|
||||
const entry = authRateMap.get(ip);
|
||||
if (!entry || now > entry.resetAt) {
|
||||
authRateMap.set(ip, { count: 1, resetAt: now + 60_000 });
|
||||
return true;
|
||||
}
|
||||
entry.count++;
|
||||
return entry.count <= maxPerMinute;
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of authRateMap) {
|
||||
if (now > entry.resetAt) authRateMap.delete(key);
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
|
||||
function generateKey(): string {
|
||||
return crypto.randomUUID();
|
||||
return randomBytes(32).toString("base64url");
|
||||
}
|
||||
|
||||
function hashEmail(email: string): string {
|
||||
return createHash("sha256").update(email.toLowerCase().trim()).digest("hex");
|
||||
return createHmac("sha256", EMAIL_HMAC_KEY).update(email.toLowerCase().trim()).digest("hex");
|
||||
}
|
||||
|
||||
async function resolveKey(key: string): Promise<{ accountId: string; keyId: string | null } | null> {
|
||||
@@ -54,16 +77,19 @@ export function requireAuth(app: Elysia) {
|
||||
|
||||
const COOKIE_OPTS = {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV !== "development",
|
||||
secure: process.env.COOKIE_SECURE !== "false",
|
||||
sameSite: "lax" as const,
|
||||
path: "/",
|
||||
domain: process.env.COOKIE_DOMAIN ?? ".pingql.com",
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
};
|
||||
|
||||
export const account = new Elysia({ prefix: "/account" })
|
||||
|
||||
.post("/login", async ({ body, cookie, set }) => {
|
||||
.post("/login", async ({ body, cookie, set, request, error }) => {
|
||||
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
||||
if (!checkAuthRateLimit(ip, 10)) return error(429, { error: "Too many login attempts. Try again later." });
|
||||
|
||||
const key = (body.key as string)?.trim();
|
||||
if (!key) { set.status = 400; return { error: "Key required" }; }
|
||||
|
||||
@@ -84,7 +110,10 @@ export const account = new Elysia({ prefix: "/account" })
|
||||
set.redirect = "/dashboard";
|
||||
}, { detail: { hide: true } })
|
||||
|
||||
.post("/register", async ({ body, cookie }) => {
|
||||
.post("/register", async ({ body, cookie, request, error }) => {
|
||||
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
||||
if (!checkAuthRateLimit(ip, 5)) return error(429, { error: "Too many registrations. Try again later." });
|
||||
|
||||
const key = generateKey();
|
||||
const emailHash = body.email ? hashEmail(body.email) : null;
|
||||
await sql`INSERT INTO accounts (key, email_hash) VALUES (${key}, ${emailHash})`;
|
||||
|
||||
Reference in New Issue
Block a user