From e39220c024c0109945111763eba80a43c9c9956a Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:09:22 +0100 Subject: [PATCH] fix: allow quoted urls in env variables (#676) --- app/server/core/config.ts | 70 ++++++++++++------- app/server/core/scheduler.ts | 2 +- app/server/lib/auth.ts | 11 +-- app/server/modules/backups/backup.helpers.ts | 2 +- app/server/modules/backups/backups.service.ts | 2 +- 5 files changed, 49 insertions(+), 38 deletions(-) diff --git a/app/server/core/config.ts b/app/server/core/config.ts index 9c9b9407..157b5edc 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -1,8 +1,10 @@ import { readFileSync } from "node:fs"; import os from "node:os"; -import { z } from "zod"; +import { prettifyError, z } from "zod"; import "dotenv/config"; +import { buildAllowedHosts } from "../lib/auth/base-url"; +const unquote = (str: string) => str.trim().replace(/^(['"])(.*)\1$/, "$2"); const getResticHostname = () => { try { const mountinfo = readFileSync("/proc/self/mountinfo", "utf-8"); @@ -41,31 +43,49 @@ const envSchema = z ENABLE_DEV_PANEL: z.string().default("false"), PROVISIONING_PATH: z.string().optional(), }) - .transform((s) => ({ - __prod__: s.NODE_ENV === "production", - environment: s.NODE_ENV, - serverIp: s.SERVER_IP, - serverIdleTimeout: s.SERVER_IDLE_TIMEOUT, - resticHostname: s.RESTIC_HOSTNAME || getResticHostname(), - port: s.PORT, - migrationsPath: s.MIGRATIONS_PATH, - appVersion: s.APP_VERSION, - trustedOrigins: s.TRUSTED_ORIGINS?.split(",") - .map((origin) => origin.trim()) - .filter(Boolean) - .concat(s.BASE_URL) ?? [s.BASE_URL], - trustProxy: s.TRUST_PROXY === "true", - disableRateLimiting: s.DISABLE_RATE_LIMITING === "true" || s.NODE_ENV === "test", - appSecret: s.APP_SECRET, - baseUrl: s.BASE_URL, - isSecure: s.BASE_URL?.startsWith("https://") ?? false, - enableDevPanel: s.ENABLE_DEV_PANEL === "true", - provisioningPath: s.PROVISIONING_PATH, - })); + .transform((s) => { + const baseUrl = unquote(s.BASE_URL); + const trustedOrigins = s.TRUSTED_ORIGINS?.split(",").map(unquote).filter(Boolean).concat(baseUrl) ?? [baseUrl]; + const authOrigins = [baseUrl, ...trustedOrigins]; + const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins); -const parseConfig = (env: unknown) => { + for (const origin of invalidOrigins) { + console.warn( + `Ignoring invalid origin in configuration: ${origin}. Make sure it is a valid URL with a protocol (e.g. https://example.com)`, + ); + } + + return { + __prod__: s.NODE_ENV === "production", + environment: s.NODE_ENV, + serverIp: s.SERVER_IP, + serverIdleTimeout: s.SERVER_IDLE_TIMEOUT, + resticHostname: s.RESTIC_HOSTNAME || getResticHostname(), + port: s.PORT, + migrationsPath: s.MIGRATIONS_PATH, + appVersion: s.APP_VERSION, + trustedOrigins: trustedOrigins, + trustProxy: s.TRUST_PROXY === "true", + disableRateLimiting: s.DISABLE_RATE_LIMITING === "true" || s.NODE_ENV === "test", + appSecret: s.APP_SECRET, + baseUrl, + isSecure: baseUrl.startsWith("https://"), + enableDevPanel: s.ENABLE_DEV_PANEL === "true", + provisioningPath: s.PROVISIONING_PATH, + allowedHosts, + }; + }); + +export const parseConfig = (env: unknown) => { const result = envSchema.safeParse(env); + if (result.success && result.data.allowedHosts.length === 0) { + console.error( + `Configuration error: No valid trusted origins provided. Please check the BASE_URL and TRUSTED_ORIGINS environment variables.`, + ); + process.exit(1); + } + if (!result.success) { if (!process.env.APP_SECRET) { const errorMessage = [ @@ -89,8 +109,8 @@ const parseConfig = (env: unknown) => { console.error(errorMessage); } - console.error(`Environment variable validation failed: ${result.error.message}`); - throw new Error("Invalid environment variables"); + console.error(`Environment variable validation failed: ${prettifyError(result.error)}`); + process.exit(1); } return result.data; diff --git a/app/server/core/scheduler.ts b/app/server/core/scheduler.ts index 756785a0..db2eef32 100644 --- a/app/server/core/scheduler.ts +++ b/app/server/core/scheduler.ts @@ -1,4 +1,4 @@ -import CronExpressionParser from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; import { logger } from "@zerobyte/core/node"; export abstract class Job { diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts index d91da47e..63a8130b 100644 --- a/app/server/lib/auth.ts +++ b/app/server/lib/auth.ts @@ -12,29 +12,20 @@ import { createAuthMiddleware } from "better-auth/api"; import { config } from "../core/config"; import { db } from "../db/db"; import { cryptoUtils } from "../utils/crypto"; -import { logger } from "@zerobyte/core/node"; import { authService } from "../modules/auth/auth.service"; import { tanstackStartCookies } from "better-auth/tanstack-start"; import { isValidUsername, normalizeUsername } from "~/lib/username"; import { ensureOnlyOneUser } from "./auth/middlewares/only-one-user"; import { convertLegacyUserOnFirstLogin } from "./auth/middlewares/convert-legacy-user"; import { ensureDefaultOrg } from "./auth/helpers/create-default-org"; -import { buildAllowedHosts } from "./auth/base-url"; import { ssoIntegration } from "../modules/sso/sso.integration"; export type AuthMiddlewareContext = MiddlewareContext>; -const authOrigins = [config.baseUrl, ...config.trustedOrigins]; -const { allowedHosts, invalidOrigins } = buildAllowedHosts(authOrigins); - -for (const origin of invalidOrigins) { - logger.warn(`Ignoring invalid auth origin in configuration: ${origin}`); -} - export const auth = betterAuth({ secret: await cryptoUtils.deriveSecret("better-auth"), baseURL: { - allowedHosts, + allowedHosts: config.allowedHosts, protocol: "auto", }, trustedOrigins: config.trustedOrigins, diff --git a/app/server/modules/backups/backup.helpers.ts b/app/server/modules/backups/backup.helpers.ts index 077e8e98..f3b05ee8 100644 --- a/app/server/modules/backups/backup.helpers.ts +++ b/app/server/modules/backups/backup.helpers.ts @@ -1,4 +1,4 @@ -import CronExpressionParser from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; import path from "node:path"; import type { BackupSchedule } from "~/server/db/schema"; import { toMessage } from "~/server/utils/errors"; diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 45beec01..1c14080f 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -1,5 +1,5 @@ import { and, eq, inArray } from "drizzle-orm"; -import CronExpressionParser from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced"; const isValidCron = (expression: string): boolean => {