mirror of
https://github.com/devkitshq/notifkit.git
synced 2026-10-19 18:24:52 +02:00
fix(security): restrict CORS to configured origin for session-auth routes
This commit is contained in:
@@ -13,6 +13,9 @@ DATABASE_URL=postgres://platform:platform@localhost:5432/notifkit
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
# Bootstrap key. Full access to every project — mint per-project keys for apps.
|
||||
ADMIN_API_KEY=my_super_secret_admin_key
|
||||
# Allowed origins for CORS on session-authenticated admin endpoints (comma-separated).
|
||||
# CORS_ORIGIN=https://dashboard.example.com
|
||||
|
||||
|
||||
# ── Unsubscribe ───────────────────────────────────────────────────────────────
|
||||
# Both are required before one-click unsubscribe headers are attached to mail.
|
||||
|
||||
@@ -70,6 +70,10 @@ export const baseConfigSchema = z.object({
|
||||
* Defaults to false to prevent client-spoofed headers from bypassing rate limits.
|
||||
*/
|
||||
TRUST_PROXY: z.coerce.boolean().default(false),
|
||||
/**
|
||||
* Comma-separated list of allowed origins for CORS on session-authenticated admin routes.
|
||||
*/
|
||||
CORS_ORIGIN: z.string().optional(),
|
||||
});
|
||||
|
||||
export type BaseConfig = z.infer<typeof baseConfigSchema>;
|
||||
|
||||
@@ -329,8 +329,28 @@ export async function startApiServer() {
|
||||
server.keepAliveTimeout = 5_000;
|
||||
|
||||
async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
// Add CORS headers for browser clients (like the Next.js dashboard)
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
||||
const origin = req.headers["origin"] as string | undefined;
|
||||
const allowedOrigins = (config.CORS_ORIGIN ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const isSessionRoute =
|
||||
url.pathname.startsWith("/v1/auth/") ||
|
||||
url.pathname === "/v1/projects" ||
|
||||
url.pathname.startsWith("/v1/projects/") ||
|
||||
url.pathname.startsWith("/v1/system/") ||
|
||||
url.pathname.startsWith("/v1/dlq");
|
||||
|
||||
if (isSessionRoute) {
|
||||
if (origin && allowedOrigins.includes(origin)) {
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
res.setHeader("Vary", "Origin");
|
||||
}
|
||||
} else {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
||||
res.setHeader(
|
||||
"Access-Control-Allow-Headers",
|
||||
@@ -343,8 +363,6 @@ export async function startApiServer() {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
||||
|
||||
if (url.pathname === "/admin" || url.pathname.startsWith("/admin/")) {
|
||||
const handled = await handleAdminRequest(req, res, url);
|
||||
if (handled) return;
|
||||
|
||||
Reference in New Issue
Block a user