From a82176c6ee0be4cd9dc7ee7b661e92fa9478ba37 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:30:42 +0100 Subject: [PATCH 01/31] feat(docker): add entrypoint script with nginx proxy for custom Seerr headers --- docker/docker-entrypoint.sh | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100755 docker/docker-entrypoint.sh diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 000000000..9b01517ae --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,57 @@ +#!/bin/sh +set -e + +CONFIG="/usr/share/nginx/html/assets/config/config.json" +NGINX_CONF="/etc/nginx/conf.d/default.conf" + +# --- Build config.json --- + +# Determine seerrProxyPath: set when both SEERR_BASE_URL and SEERR_CUSTOM_HEADERS are provided +if [ -n "$SEERR_BASE_URL" ] && [ -n "$SEERR_CUSTOM_HEADERS" ]; then + SEERR_PROXY_PATH="/seerr-proxy" +else + SEERR_PROXY_PATH="" +fi + +SEERR_PROXY_JSON=$([ -n "$SEERR_PROXY_PATH" ] && echo "\"$SEERR_PROXY_PATH\"" || echo null) + +cat > "$CONFIG" < "$NGINX_CONF" < Date: Mon, 16 Mar 2026 00:31:07 +0100 Subject: [PATCH 02/31] feat(docker): add jq and custom headers env var in Dockerfile --- Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 23c79b92d..286d7cab1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,17 @@ FROM nginx:alpine +RUN apk add --no-cache jq + EXPOSE 80 ENV BASE_URL="" ENV SEERR_BASE_URL="" -ENV SEERR_HEADER="null" +ENV SEERR_CUSTOM_HEADERS="" +ENV PORT=80 COPY build/web /usr/share/nginx/html -COPY docker-entrypoint.sh /docker-entrypoint.sh +COPY docker/docker-entrypoint.sh /docker-entrypoint.sh -RUN mkdir -p /usr/share/nginx/html/assets/config && \ - chmod +x /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh CMD ["/docker-entrypoint.sh"] From e523035e5304a61981b4b97127e740778a3d55db Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:31:35 +0100 Subject: [PATCH 03/31] feat(docker): add jq and custom headers env var in Dockerfile-rootless --- Dockerfile-rootless | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index 8a77ed666..f23c821b4 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -1,18 +1,21 @@ FROM ghcr.io/nginxinc/nginx-unprivileged:stable-alpine-slim +RUN apk add --no-cache jq + EXPOSE 8080 ENV BASE_URL="" ENV SEERR_BASE_URL="" -ENV SEERR_HEADER="null" +ENV SEERR_CUSTOM_HEADERS="" +ENV PORT=8080 USER root COPY build/web /usr/share/nginx/html -COPY docker-entrypoint.sh /docker-entrypoint.sh -RUN mkdir -p /usr/share/nginx/html/assets/config && \ - chown -R nginx:nginx /usr/share/nginx/html && \ +COPY docker/docker-entrypoint.sh /docker-entrypoint.sh +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /etc/nginx/conf.d && \ chmod +x /docker-entrypoint.sh && \ chown nginx:nginx /docker-entrypoint.sh USER nginx -CMD ["/docker-entrypoint.sh"] \ No newline at end of file +CMD ["/docker-entrypoint.sh"] From aac4281bc27fb88626584d7d71209ac7fd8e09dc Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:31:53 +0100 Subject: [PATCH 04/31] refactor(docker): remove old root-level docker-entrypoint.sh --- docker-entrypoint.sh | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100644 index 843020f6c..000000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -e - -# Generate config.json from environment variables -cat > /usr/share/nginx/html/assets/config/config.json < Date: Mon, 16 Mar 2026 00:32:22 +0100 Subject: [PATCH 05/31] feat(docker): replace SEERR_HEADER with SEERR_CUSTOM_HEADERS in docker-compose.yml --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d321b3857..eccfd7a87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,4 +7,4 @@ services: environment: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server - SEERR_BASE_URL=https://seerr-url #OPTIONAL: Presets Seerr base URL - - SEERR_HEADER={"key":"value"} #OPTIONAL: JSON object string of Seerr headers + - SEERR_CUSTOM_HEADERS={"key":"value"} #OPTIONAL: JSON object of custom headers injected via nginx proxy From 35b4cd16704bd6eaa58fd29678e47a43f01d33ff Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:32:56 +0100 Subject: [PATCH 06/31] feat(config): add seerrProxyPath field for nginx proxy routing --- lib/util/fladder_config.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/util/fladder_config.dart b/lib/util/fladder_config.dart index 0289ef89d..26d04f121 100644 --- a/lib/util/fladder_config.dart +++ b/lib/util/fladder_config.dart @@ -10,15 +10,21 @@ class FladderConfig { static set seerrBaseUrl(String? value) => _instance._seerrBaseUrl = value; String? _seerrBaseUrl; + static String? get seerrProxyPath => _instance._seerrProxyPath; + static set seerrProxyPath(String? value) => _instance._seerrProxyPath = value; + String? _seerrProxyPath; + static void fromJson(Map json) => _instance = FladderConfig._fromJson(json); factory FladderConfig._fromJson(Map json) { final config = FladderConfig._(); final newUrl = json['baseUrl'] as String?; final newSeerrUrl = json['seerrBaseUrl'] as String?; + final newProxyPath = json['seerrProxyPath'] as String?; config._baseUrl = newUrl?.isEmpty == true ? null : newUrl; config._seerrBaseUrl = newSeerrUrl?.isEmpty == true ? null : newSeerrUrl; + config._seerrProxyPath = newProxyPath?.isEmpty == true ? null : newProxyPath; return config; } From ffa0667efdd7a437156632efdd3ab5cf3b3c09ba Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:33:20 +0100 Subject: [PATCH 07/31] feat(config): add seerrProxyPath and remove stale seerrHeader --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index 697d6235c..a03ad8df0 100644 --- a/config/config.json +++ b/config/config.json @@ -1,5 +1,5 @@ { "baseUrl": null, "seerrBaseUrl": null, - "seerrHeader": null + "seerrProxyPath": null } \ No newline at end of file From 35adb92dc1e9215ef58500202e59144792c3cf9f Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:34:23 +0100 Subject: [PATCH 08/31] feat(seerr): route requests through nginx proxy when seerrProxyPath is set --- lib/providers/seerr_api_provider.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index 6f22c59cb..1c2c81c1d 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:chopper/chopper.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -62,7 +63,14 @@ class SeerrRequest implements Interceptor { ...?creds?.customHeaders, }; final headers = {...authHeaders, ...customHeaders}; - final apiBaseUri = Uri.parse(serverUrl); + var apiBaseUri = Uri.parse(serverUrl); + + // On web, route through the same-origin nginx proxy when seerrProxyPath is set. + // Nginx injects custom headers server-side so secrets never reach the browser. + final proxyPath = FladderConfig.seerrProxyPath; + if (kIsWeb && proxyPath != null) { + apiBaseUri = Uri.base.resolve(proxyPath); + } Uri resolvedRequestUri; try { From caecbc82e8e49ce064211859a605b85fa2e9a6b0 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:55:55 +0100 Subject: [PATCH 09/31] fix(docker): keep entrypoint at root and use existing SEERR_HEADER env var --- docker/docker-entrypoint.sh => docker-entrypoint.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename docker/docker-entrypoint.sh => docker-entrypoint.sh (77%) diff --git a/docker/docker-entrypoint.sh b/docker-entrypoint.sh similarity index 77% rename from docker/docker-entrypoint.sh rename to docker-entrypoint.sh index 9b01517ae..106ea43c9 100755 --- a/docker/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -6,8 +6,8 @@ NGINX_CONF="/etc/nginx/conf.d/default.conf" # --- Build config.json --- -# Determine seerrProxyPath: set when both SEERR_BASE_URL and SEERR_CUSTOM_HEADERS are provided -if [ -n "$SEERR_BASE_URL" ] && [ -n "$SEERR_CUSTOM_HEADERS" ]; then +# Determine seerrProxyPath: set when both SEERR_BASE_URL and SEERR_HEADER are provided +if [ -n "$SEERR_BASE_URL" ] && [ -n "$SEERR_HEADER" ] && [ "$SEERR_HEADER" != "null" ]; then SEERR_PROXY_PATH="/seerr-proxy" else SEERR_PROXY_PATH="" @@ -26,9 +26,9 @@ EOF # --- Build nginx config --- PROXY_BLOCK="" -if [ -n "$SEERR_BASE_URL" ] && [ -n "$SEERR_CUSTOM_HEADERS" ]; then +if [ -n "$SEERR_BASE_URL" ] && [ -n "$SEERR_HEADER" ] && [ "$SEERR_HEADER" != "null" ]; then # Build proxy_set_header directives from JSON object - HEADER_DIRECTIVES=$(echo "$SEERR_CUSTOM_HEADERS" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') + HEADER_DIRECTIVES=$(echo "$SEERR_HEADER" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') PROXY_BLOCK=" location ${SEERR_PROXY_PATH}/ { From e36ebb77441e8ea07a9355f2b92dc2caac2f9aaa Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:56:32 +0100 Subject: [PATCH 10/31] fix(docker): minimize Dockerfile diff - keep SEERR_HEADER and mkdir --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 286d7cab1..ff0343f5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,12 +6,13 @@ EXPOSE 80 ENV BASE_URL="" ENV SEERR_BASE_URL="" -ENV SEERR_CUSTOM_HEADERS="" +ENV SEERR_HEADER="null" ENV PORT=80 COPY build/web /usr/share/nginx/html -COPY docker/docker-entrypoint.sh /docker-entrypoint.sh +COPY docker-entrypoint.sh /docker-entrypoint.sh -RUN chmod +x /docker-entrypoint.sh +RUN mkdir -p /usr/share/nginx/html/assets/config && \ + chmod +x /docker-entrypoint.sh CMD ["/docker-entrypoint.sh"] From 07adf2704cea21bda5e2fdac855db2acbf61ed18 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:57:00 +0100 Subject: [PATCH 11/31] fix(docker): minimize Dockerfile-rootless diff - keep SEERR_HEADER and mkdir --- Dockerfile-rootless | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index f23c821b4..bcc675ecf 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -6,13 +6,14 @@ EXPOSE 8080 ENV BASE_URL="" ENV SEERR_BASE_URL="" -ENV SEERR_CUSTOM_HEADERS="" +ENV SEERR_HEADER="null" ENV PORT=8080 USER root COPY build/web /usr/share/nginx/html -COPY docker/docker-entrypoint.sh /docker-entrypoint.sh -RUN chown -R nginx:nginx /usr/share/nginx/html && \ +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN mkdir -p /usr/share/nginx/html/assets/config && \ + chown -R nginx:nginx /usr/share/nginx/html && \ chown -R nginx:nginx /etc/nginx/conf.d && \ chmod +x /docker-entrypoint.sh && \ chown nginx:nginx /docker-entrypoint.sh From 74da47d054046a47a0ac3ce901ef30df9bb4b57e Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:57:37 +0100 Subject: [PATCH 12/31] fix(docker): revert docker-compose.yml to upstream (SEERR_HEADER already correct) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index eccfd7a87..d321b3857 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,4 +7,4 @@ services: environment: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server - SEERR_BASE_URL=https://seerr-url #OPTIONAL: Presets Seerr base URL - - SEERR_CUSTOM_HEADERS={"key":"value"} #OPTIONAL: JSON object of custom headers injected via nginx proxy + - SEERR_HEADER={"key":"value"} #OPTIONAL: JSON object string of Seerr headers From 0489d7a4c0db7a1efc9cda8498e7144760545029 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:58:04 +0100 Subject: [PATCH 13/31] fix(config): keep existing seerrHeader, only add seerrProxyPath --- config/config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.json b/config/config.json index a03ad8df0..8f934ab7d 100644 --- a/config/config.json +++ b/config/config.json @@ -1,5 +1,6 @@ { "baseUrl": null, "seerrBaseUrl": null, + "seerrHeader": null, "seerrProxyPath": null } \ No newline at end of file From 3d0e6b95a6638305e0ad03bec489acd5f22dd9dc Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:11:58 +0100 Subject: [PATCH 14/31] fix(docker): move apk add after USER root in Dockerfile-rootless --- Dockerfile-rootless | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index bcc675ecf..49536b297 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -1,7 +1,5 @@ FROM ghcr.io/nginxinc/nginx-unprivileged:stable-alpine-slim -RUN apk add --no-cache jq - EXPOSE 8080 ENV BASE_URL="" @@ -10,6 +8,7 @@ ENV SEERR_HEADER="null" ENV PORT=8080 USER root +RUN apk add --no-cache jq COPY build/web /usr/share/nginx/html COPY docker-entrypoint.sh /docker-entrypoint.sh RUN mkdir -p /usr/share/nginx/html/assets/config && \ From 1ac2287c88d3cabca880f2554486f0010bcb2e07 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:53:53 +0200 Subject: [PATCH 15/31] chore(config): remove vestigial seerrHeader field from example config --- config/config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config.json b/config/config.json index 8f934ab7d..a03ad8df0 100644 --- a/config/config.json +++ b/config/config.json @@ -1,6 +1,5 @@ { "baseUrl": null, "seerrBaseUrl": null, - "seerrHeader": null, "seerrProxyPath": null } \ No newline at end of file From 98d403cbb1f141e0bf8df91c20258638a93ee5a2 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:54:31 +0200 Subject: [PATCH 16/31] docs(compose): clarify SEERR_HEADER comment reflects nginx proxy injection --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8625a2cf9..0f68170ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,5 +7,5 @@ services: environment: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server - SEERR_BASE_URL=https://seerr-url #OPTIONAL: Presets Seerr base URL - - SEERR_HEADER={"key":"value"} #OPTIONAL: JSON object string of Seerr headers + - SEERR_HEADER={"key":"value"} #OPTIONAL: JSON headers injected server-side via nginx proxy (requires SEERR_BASE_URL) - FLADDER_WEBPATH=/ #OPTIONAL: Configures a subpath to run Fladder at From b32f8ccfd2118ca2069523ca9f6d4a935ba06839 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:24:05 +0200 Subject: [PATCH 17/31] perf(seerr): skip redundant Uri.parse on web proxy path --- lib/providers/seerr_api_provider.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index 1c2c81c1d..69542b140 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -63,14 +63,10 @@ class SeerrRequest implements Interceptor { ...?creds?.customHeaders, }; final headers = {...authHeaders, ...customHeaders}; - var apiBaseUri = Uri.parse(serverUrl); - // On web, route through the same-origin nginx proxy when seerrProxyPath is set. // Nginx injects custom headers server-side so secrets never reach the browser. final proxyPath = FladderConfig.seerrProxyPath; - if (kIsWeb && proxyPath != null) { - apiBaseUri = Uri.base.resolve(proxyPath); - } + final apiBaseUri = (kIsWeb && proxyPath != null) ? Uri.base.resolve(proxyPath) : Uri.parse(serverUrl); Uri resolvedRequestUri; try { From 07f20e5594cc3dd1fcca972492f50fad36793ac4 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:24:58 +0200 Subject: [PATCH 18/31] refactor(entrypoint): reuse SEERR_PROXY_PATH as single proxy-enabled flag --- docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 51a6472b5..389520b18 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -37,7 +37,7 @@ PORT="${PORT:-$([ "$(id -u)" = "0" ] && echo 80 || echo 8080)}" # --- Build Seerr proxy block --- PROXY_BLOCK="" -if [ -n "$SEERR_BASE_URL" ] && [ -n "$SEERR_HEADER" ] && [ "$SEERR_HEADER" != "null" ]; then +if [ -n "$SEERR_PROXY_PATH" ]; then HEADER_DIRECTIVES=$(echo "$SEERR_HEADER" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') PROXY_BLOCK=" From 0de4db66dc6a775b46c2ba94c2b24231a5375508 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:25:55 +0200 Subject: [PATCH 19/31] fix(entrypoint): strip trailing slash from SEERR_BASE_URL to avoid double slash in proxy_pass --- docker-entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 389520b18..9b9e3590c 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -4,6 +4,9 @@ set -e CONFIG="/usr/share/nginx/html/assets/config/config.json" NGINX_CONF="/etc/nginx/conf.d/default.conf" +# Strip trailing slashes from SEERR_BASE_URL so proxy_pass doesn't emit a double slash. +SEERR_BASE_URL=$(echo "$SEERR_BASE_URL" | sed 's|/*$||') + # --- Build config.json --- # Determine seerrProxyPath: set when both SEERR_BASE_URL and SEERR_HEADER are provided From 0bb5a79ad2fe8412da605c4701e5418822abaf9a Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:26:44 +0200 Subject: [PATCH 20/31] fix(entrypoint): preserve upstream's empty-string behavior for baseUrl/seerrBaseUrl --- docker-entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 9b9e3590c..7f64f458a 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -20,8 +20,8 @@ SEERR_PROXY_JSON=$([ -n "$SEERR_PROXY_PATH" ] && echo "\"$SEERR_PROXY_PATH\"" || cat > "$CONFIG" < Date: Wed, 22 Apr 2026 22:37:40 +0200 Subject: [PATCH 21/31] fix(entrypoint): harden SEERR_HEADER escaping, support $ in values --- docker-entrypoint.sh | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7f64f458a..849d09679 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -40,9 +40,25 @@ PORT="${PORT:-$([ "$(id -u)" = "0" ] && echo 80 || echo 8080)}" # --- Build Seerr proxy block --- PROXY_BLOCK="" +GEO_BLOCK="" if [ -n "$SEERR_PROXY_PATH" ]; then - HEADER_DIRECTIVES=$(echo "$SEERR_HEADER" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') - + # CR/LF are forbidden in HTTP header values (RFC 9110 §5.5) — reject rather than escape. + if printf '%s' "$SEERR_HEADER" | jq -e '[.[] | test("[\r\n]")] | any' > /dev/null; then + echo "Error: SEERR_HEADER values must not contain newline or carriage return (invalid HTTP header values)" >&2 + exit 1 + fi + # nginx has no native escape for literal '$' in quoted strings; define a variable + # holding "$" at http level and substitute '$' -> '${literal_dollar}' in values. + # tojson handles " and \ escaping (nginx shares those conventions with JSON). + HEADER_DIRECTIVES=$(printf '%s' "$SEERR_HEADER" | jq -r ' + to_entries[] | + " proxy_set_header " + .key + " " + (.value | gsub("\\$"; "${literal_dollar}") | tojson) + ";" + ') + + GEO_BLOCK='geo $literal_dollar { + default "$"; +} +' PROXY_BLOCK=" location ${SEERR_PROXY_PATH}/ { proxy_pass ${SEERR_BASE_URL}/; @@ -56,7 +72,7 @@ fi if [ "$WEBPATH" = "/" ]; then echo "Configuring Fladder at root path" cat > "$NGINX_CONF" < "$NGINX_CONF" < Date: Wed, 22 Apr 2026 22:46:08 +0200 Subject: [PATCH 22/31] fix(entrypoint): namespace nginx literal-dollar variable to avoid collisions --- docker-entrypoint.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 849d09679..43477ea27 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -47,15 +47,16 @@ if [ -n "$SEERR_PROXY_PATH" ]; then echo "Error: SEERR_HEADER values must not contain newline or carriage return (invalid HTTP header values)" >&2 exit 1 fi - # nginx has no native escape for literal '$' in quoted strings; define a variable - # holding "$" at http level and substitute '$' -> '${literal_dollar}' in values. + # nginx has no native escape for literal '$' in quoted strings; define a namespaced + # variable holding "$" at http level and substitute '$' -> '${seerr_literal_dollar}' + # in values. Namespace prefix avoids collision with user-mounted nginx configs. # tojson handles " and \ escaping (nginx shares those conventions with JSON). HEADER_DIRECTIVES=$(printf '%s' "$SEERR_HEADER" | jq -r ' to_entries[] | - " proxy_set_header " + .key + " " + (.value | gsub("\\$"; "${literal_dollar}") | tojson) + ";" + " proxy_set_header " + .key + " " + (.value | gsub("\\$"; "${seerr_literal_dollar}") | tojson) + ";" ') - GEO_BLOCK='geo $literal_dollar { + GEO_BLOCK='geo $seerr_literal_dollar { default "$"; } ' From 6355b7ecb98aa9fe08830158547e5eba3eb08d37 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:47:21 +0200 Subject: [PATCH 23/31] refactor(entrypoint): extract strip_trailing_slashes helper --- docker-entrypoint.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 43477ea27..f7a2acfe3 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -4,8 +4,12 @@ set -e CONFIG="/usr/share/nginx/html/assets/config/config.json" NGINX_CONF="/etc/nginx/conf.d/default.conf" +strip_trailing_slashes() { + echo "$1" | sed 's|/*$||' +} + # Strip trailing slashes from SEERR_BASE_URL so proxy_pass doesn't emit a double slash. -SEERR_BASE_URL=$(echo "$SEERR_BASE_URL" | sed 's|/*$||') +SEERR_BASE_URL=$(strip_trailing_slashes "$SEERR_BASE_URL") # --- Build config.json --- @@ -88,7 +92,7 @@ ${PROXY_BLOCK} EOF else echo "Configuring Fladder on subpath: $WEBPATH" - WEBPATH_NO_SLASH=$(echo "$WEBPATH" | sed 's|/*$||') + WEBPATH_NO_SLASH=$(strip_trailing_slashes "$WEBPATH") cat > "$NGINX_CONF" < Date: Wed, 22 Apr 2026 22:48:47 +0200 Subject: [PATCH 24/31] docs(entrypoint): clarify the two $-escape conventions in PROXY_BLOCK --- docker-entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index f7a2acfe3..7ac1360d5 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -64,6 +64,9 @@ if [ -n "$SEERR_PROXY_PATH" ]; then default "$"; } ' + # Two '$' conventions coexist below: + # \$proxy_host — backslash escapes the shell; nginx sees $proxy_host (built-in variable). + # ${seerr_literal_dollar} — nginx-level reference to the geo-defined variable above, emits a literal '$'. PROXY_BLOCK=" location ${SEERR_PROXY_PATH}/ { proxy_pass ${SEERR_BASE_URL}/; From 382b2036ecda9305dffe68908dcfc4d150d87495 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:54:12 +0200 Subject: [PATCH 25/31] docs(seerr): reword proxy comment --- lib/providers/seerr_api_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index 69542b140..1aa167913 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -64,7 +64,7 @@ class SeerrRequest implements Interceptor { }; final headers = {...authHeaders, ...customHeaders}; // On web, route through the same-origin nginx proxy when seerrProxyPath is set. - // Nginx injects custom headers server-side so secrets never reach the browser. + // Nginx injects the headers server-side so the header values never reach the browser. final proxyPath = FladderConfig.seerrProxyPath; final apiBaseUri = (kIsWeb && proxyPath != null) ? Uri.base.resolve(proxyPath) : Uri.parse(serverUrl); From 13f4abae287960b4bbe437d8855371f4493b11ab Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:55:55 +0200 Subject: [PATCH 26/31] refactor(fladder_config): extract _nonEmpty helper for json string fields --- lib/util/fladder_config.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/util/fladder_config.dart b/lib/util/fladder_config.dart index 26d04f121..5d4af1847 100644 --- a/lib/util/fladder_config.dart +++ b/lib/util/fladder_config.dart @@ -18,14 +18,11 @@ class FladderConfig { factory FladderConfig._fromJson(Map json) { final config = FladderConfig._(); - final newUrl = json['baseUrl'] as String?; - final newSeerrUrl = json['seerrBaseUrl'] as String?; - final newProxyPath = json['seerrProxyPath'] as String?; - - config._baseUrl = newUrl?.isEmpty == true ? null : newUrl; - config._seerrBaseUrl = newSeerrUrl?.isEmpty == true ? null : newSeerrUrl; - config._seerrProxyPath = newProxyPath?.isEmpty == true ? null : newProxyPath; - + config._baseUrl = _nonEmpty(json['baseUrl'] as String?); + config._seerrBaseUrl = _nonEmpty(json['seerrBaseUrl'] as String?); + config._seerrProxyPath = _nonEmpty(json['seerrProxyPath'] as String?); return config; } + + static String? _nonEmpty(String? s) => s?.isEmpty == true ? null : s; } From 1f10aa3b66906c46bcf07e334ef83810ab7cd5c4 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:28:16 +0200 Subject: [PATCH 27/31] fix(entrypoint): validate SEERR_HEADER shape before parsing --- docker-entrypoint.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7ac1360d5..ae5600317 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -46,6 +46,12 @@ PORT="${PORT:-$([ "$(id -u)" = "0" ] && echo 80 || echo 8080)}" PROXY_BLOCK="" GEO_BLOCK="" if [ -n "$SEERR_PROXY_PATH" ]; then + # SEERR_HEADER must be a JSON object of strings. Validate up front so the jq + # iterations below don't abort with cryptic "cannot iterate over..." errors. + if ! printf '%s' "$SEERR_HEADER" | jq -e 'type == "object" and all(.[]; type == "string")' > /dev/null 2>&1; then + echo "Error: SEERR_HEADER must be a JSON object with string values, e.g. {\"Header-Name\":\"value\"}" >&2 + exit 1 + fi # CR/LF are forbidden in HTTP header values (RFC 9110 §5.5) — reject rather than escape. if printf '%s' "$SEERR_HEADER" | jq -e '[.[] | test("[\r\n]")] | any' > /dev/null; then echo "Error: SEERR_HEADER values must not contain newline or carriage return (invalid HTTP header values)" >&2 From 9523bb339d2c1d812f5f2b93636ca38ecc5bcc78 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:30:20 +0200 Subject: [PATCH 28/31] fix(entrypoint): validate SEERR_HEADER keys to prevent nginx directive injection --- docker-entrypoint.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index ae5600317..379f98026 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -52,6 +52,13 @@ if [ -n "$SEERR_PROXY_PATH" ]; then echo "Error: SEERR_HEADER must be a JSON object with string values, e.g. {\"Header-Name\":\"value\"}" >&2 exit 1 fi + # Keys are injected unquoted into the nginx directive — restrict to the common + # HTTP header-name form (alpha-start, then alphanumerics and hyphens) so a key + # containing whitespace or ';' can't escape the proxy_set_header directive. + if ! printf '%s' "$SEERR_HEADER" | jq -e 'all(keys_unsorted[]; test("^[A-Za-z][A-Za-z0-9-]*$"))' > /dev/null 2>&1; then + echo "Error: SEERR_HEADER keys must match ^[A-Za-z][A-Za-z0-9-]*\$ (standard HTTP header name format)" >&2 + exit 1 + fi # CR/LF are forbidden in HTTP header values (RFC 9110 §5.5) — reject rather than escape. if printf '%s' "$SEERR_HEADER" | jq -e '[.[] | test("[\r\n]")] | any' > /dev/null; then echo "Error: SEERR_HEADER values must not contain newline or carriage return (invalid HTTP header values)" >&2 From 554f5cbdc9cfd84961936618dbcf2f68c0bee196 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:31:33 +0200 Subject: [PATCH 29/31] fix(entrypoint): quote SEERR_BASE_URL in proxy_pass to prevent directive injection --- docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 379f98026..3fea2aae9 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -82,7 +82,7 @@ if [ -n "$SEERR_PROXY_PATH" ]; then # ${seerr_literal_dollar} — nginx-level reference to the geo-defined variable above, emits a literal '$'. PROXY_BLOCK=" location ${SEERR_PROXY_PATH}/ { - proxy_pass ${SEERR_BASE_URL}/; + proxy_pass \"${SEERR_BASE_URL}/\"; proxy_set_header Host \$proxy_host; proxy_ssl_server_name on; ${HEADER_DIRECTIVES} From b3f3d95094e64bed592417ac5448a8238218652a Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:35:45 +0200 Subject: [PATCH 30/31] docs(compose): warn against using Seerr's X-Api-Key in SEERR_HEADER --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 0f68170ef..240e56f6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,5 +7,9 @@ services: environment: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server - SEERR_BASE_URL=https://seerr-url #OPTIONAL: Presets Seerr base URL + # SEERR_HEADER is intended to e.g. bypass an OUTER auth wall which is in front of Seerr: + # Authelia, Authentik, forward-auth, Cloudflare Access, basic-auth on a reverse proxy, etc.) + # Do NOT put Seerr's own X-Api-Key here! + # /seerr-proxy/ is not authenticated by Fladder, so an X-Api-Key would let any HTTP caller act as a Seerr admin. - SEERR_HEADER={"key":"value"} #OPTIONAL: JSON headers injected server-side via nginx proxy (requires SEERR_BASE_URL) - FLADDER_WEBPATH=/ #OPTIONAL: Configures a subpath to run Fladder at From 52b3246b436dcdae262d2b41c9043d1b9e8d7638 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:56:34 +0200 Subject: [PATCH 31/31] fix(seerr): reject non-same-origin seerrProxyPath to prevent config-tampering redirect --- lib/providers/seerr_api_provider.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index 1aa167913..5b5d3796f 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -65,8 +65,11 @@ class SeerrRequest implements Interceptor { final headers = {...authHeaders, ...customHeaders}; // On web, route through the same-origin nginx proxy when seerrProxyPath is set. // Nginx injects the headers server-side so the header values never reach the browser. + // Require a same-origin absolute path (starts with '/' but not '//') so a tampered + // config.json can't redirect Seerr traffic to a foreign host via Uri.base.resolve. final proxyPath = FladderConfig.seerrProxyPath; - final apiBaseUri = (kIsWeb && proxyPath != null) ? Uri.base.resolve(proxyPath) : Uri.parse(serverUrl); + final isSafeProxyPath = proxyPath != null && proxyPath.startsWith('/') && !proxyPath.startsWith('//'); + final apiBaseUri = (kIsWeb && isSafeProxyPath) ? Uri.base.resolve(proxyPath) : Uri.parse(serverUrl); Uri resolvedRequestUri; try {