From 9632c77177f109e0e85ede274b8ca78575f67134 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:08:08 +0100 Subject: [PATCH] feat: add a trust proxy env to control whether to trust incoming x-forwarded-for headers (#662) --- .env.example | 1 + README.md | 1 + app/routes/api.$.ts | 18 +++++++++++++++++- app/server/core/config.ts | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 52a8269e..321cc1e8 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,5 @@ ZEROBYTE_REPOSITORIES_DIR=./data/repositories ZEROBYTE_VOLUMES_DIR=./data/volumes APP_SECRET= BASE_URL=http://localhost:300 +TRUST_PROXY=false PORT=3000 diff --git a/README.md b/README.md index 71d72777..f15c02bb 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Zerobyte can be customized using environment variables. Below are the available | `PORT` | The port the web interface and API will listen on. | `4096` | | `RESTIC_HOSTNAME` | The hostname used by Restic when creating snapshots. Automatically detected if a custom hostname is set in Docker. | `zerobyte` | | `TZ` | Timezone for the container (e.g., `Europe/Zurich`). **Crucial for accurate backup scheduling.** | `UTC` | +| `TRUST_PROXY` | When `true`, trust an existing `X-Forwarded-For` header from your reverse proxy. Leave `false` for direct deployments. | `false` | | `TRUSTED_ORIGINS` | Comma-separated list of extra trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) | | `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` | | `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` | diff --git a/app/routes/api.$.ts b/app/routes/api.$.ts index f48ecfc5..80bdbb0e 100644 --- a/app/routes/api.$.ts +++ b/app/routes/api.$.ts @@ -5,6 +5,7 @@ import { config } from "~/server/core/config"; const app = createApp(); type NodeRuntimeRequest = Request & { + ip?: string; runtime?: { node?: { res?: { setTimeout: (timeoutMs: number) => void }; @@ -16,7 +17,22 @@ export const prepareApiRequest = (request: Request, timeoutMs: number) => { const nodeRequest = request as NodeRuntimeRequest; nodeRequest.runtime?.node?.res?.setTimeout(timeoutMs); - return request.clone(); + if (config.trustProxy && request.headers.has("x-forwarded-for")) { + return request.clone(); + } + + const remoteAddress = nodeRequest.ip; + if (remoteAddress) { + const headers = new Headers(request.headers); + headers.set("x-forwarded-for", remoteAddress); + + return new Request(request, { headers }); + } + + const headers = new Headers(request.headers); + headers.delete("x-forwarded-for"); + + return new Request(request, { headers }); }; const handle = ({ request }: { request: Request }) => diff --git a/app/server/core/config.ts b/app/server/core/config.ts index 1b3b006f..9f4247d6 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -34,6 +34,7 @@ const envSchema = z MIGRATIONS_PATH: z.string().optional(), APP_VERSION: z.string().default("dev"), TRUSTED_ORIGINS: z.string().optional(), + TRUST_PROXY: z.string().default("false"), DISABLE_RATE_LIMITING: z.string().default("false"), APP_SECRET: z.string().min(32).max(256), BASE_URL: z.string(), @@ -53,6 +54,7 @@ const envSchema = z .map((origin) => origin.trim()) .filter(Boolean) .concat(s.BASE_URL) ?? [s.BASE_URL], + trustProxy: s.TRUST_PROXY === "true", disableRateLimiting: s.DISABLE_RATE_LIMITING === "true", appSecret: s.APP_SECRET, baseUrl: s.BASE_URL,