diff --git a/.github/workflows/caddy.yml b/.github/workflows/caddy.yml index 2e6b250f9b..2d0d2a8ed5 100644 --- a/.github/workflows/caddy.yml +++ b/.github/workflows/caddy.yml @@ -7,14 +7,12 @@ on: - .github/workflows/caddy.yml - caddy/Dockerfile - caddy/Caddyfile - - caddy/cel-go-0.29.patch - caddy/source-check.sh pull_request: paths: - .github/workflows/caddy.yml - caddy/Dockerfile - caddy/Caddyfile - - caddy/cel-go-0.29.patch - caddy/source-check.sh workflow_dispatch: permissions: diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index 2a38eca789..8c0413f143 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -25,6 +25,12 @@ const internalAccessList = { */ create: async (access, data) => { access.can("access_lists:manage"); + if ((data.items || []).some((item) => !item.password)) { + throw Object.assign(new errs.ValidationError("A password is required for new access-list users"), { + message_i18n: "error.access.password-required", + }); + } + const row = await accessListModel.query().insertAndFetch({ name: data.name, satisfy_any: data.satisfy_any, diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 0d81cd00cf..60b8d22c6b 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -31,6 +31,7 @@ const internalCertificate = { intervalProcessing: false, initTimer: () => { + if (internalCertificate.interval) return; logger.info("Certbot Renewal Timer initialized"); internalCertificate.interval = setInterval( internalCertificate.processExpiringHosts, diff --git a/backend/internal/ip_ranges.js b/backend/internal/ip_ranges.js index 7ae9923a97..961e5ed0eb 100644 --- a/backend/internal/ip_ranges.js +++ b/backend/internal/ip_ranges.js @@ -22,6 +22,7 @@ const internalIpRanges = { interval_processing: false, initTimer: () => { + if (internalIpRanges.interval) return; logger.info("IP Ranges Renewal Timer initialized"); internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout); }, diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json index 23232e9bdf..c43f133165 100644 --- a/backend/schema/components/redirection-host-object.json +++ b/backend/schema/components/redirection-host-object.json @@ -51,7 +51,6 @@ "forward_scheme": { "type": "string", "enum": [ - "auto", "$scheme", "http", "https" diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json index 9619e20d93..2a3c007366 100644 --- a/backend/schema/paths/nginx/redirection-hosts/post.json +++ b/backend/schema/paths/nginx/redirection-hosts/post.json @@ -88,7 +88,7 @@ "test.example.com" ], "forward_domain_name": "example.com", - "forward_scheme": "auto", + "forward_scheme": "$scheme", "forward_http_code": 301, "preserve_path": false, "certificate_id": 0, @@ -130,7 +130,7 @@ "enabled": true, "hsts_enabled": false, "hsts_subdomains": false, - "forward_scheme": "auto", + "forward_scheme": "$scheme", "forward_http_code": 301 } } diff --git a/backend/setup.js b/backend/setup.js index 69b81557a8..7fdf3533b0 100644 --- a/backend/setup.js +++ b/backend/setup.js @@ -263,6 +263,7 @@ const setupAio = async () => { npmplus_crowdsec_appsec: false, npmplus_proxy_request_buffering: true, npmplus_proxy_response_buffering: true, + npmplus_upstream_compression: true, certificate_id: "new", ssl_forced: true, hsts_enabled: true, diff --git a/backend/test/access-list-recovery.test.js b/backend/test/access-list-recovery.test.js index 811a37810a..112326e5de 100644 --- a/backend/test/access-list-recovery.test.js +++ b/backend/test/access-list-recovery.test.js @@ -34,6 +34,17 @@ const seed = async () => { return list; }; +test("a blank password on create rejects before inserting the list or any credentials", async () => { + await assert.rejects( + internalAccessList.create(access, { + name: "Created", + items: [{ username: "user", password: "" }], + }), + (error) => error.status === 400, + ); + assert.equal(await AccessList.query().where("name", "Created").first(), undefined); +}); + test("a blank-password rename rejects before changing any access-list fields or credentials", async (t) => { t.mock.method(internalNginx, "reload", async () => {}); t.mock.method(internalNginx, "test", async () => {}); diff --git a/caddy/Dockerfile b/caddy/Dockerfile index 34227d634f..b69abb146d 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -1,51 +1,19 @@ # syntax=docker/dockerfile:1.27.1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e FROM golang:1.26.8-alpine3.23@sha256:33ce311e5eecedee48ec1b84419c1306e9fbd71009f0d5c3f2a6904b579c1ecc AS builder -ARG CADDY_VERSION=v2.11.4 +ARG CADDY_VERSION=v2.11.6 ARG XCADDY_VERSION=v0.4.5 -ARG X_CRYPTO_VERSION=v0.56.0 -ARG X_NET_VERSION=v0.57.0 -# v1.83.2 fixes GHSA-2v4p-qf9q-27wj (CVE-2026-84445): xDS servers crash on -# requests missing both :authority and Host headers. -ARG GRPC_VERSION=v1.83.2 -ARG CEL_GO_VERSION=v0.30.0 -ARG CHI_VERSION=v5.3.0 -ARG COMPRESS_VERSION=v1.18.7 -# Caddy 2.11.4 pins the OTLP log modules at v0.19.0. That exporter accepted a -# TLS certificate without validating it (CVE-2026-81871, fixed in v0.21.0). -# The log modules carry their own v0.x line, so pin the whole set to the -# aligned otel v1.45.0 release, including the trace exporters (CVE-2026-81870). -ARG OTEL_VERSION=v1.45.0 -ARG OTEL_LOG_VERSION=v0.21.0 - -# Caddy 2.11.4 predates cel-go 0.29's InterpretableV2 API. This two-line -# compatibility patch is taken from upstream commit b2693fb63a30e6d7be0972c3645e9a2c0a500e93. -COPY cel-go-0.29.patch /tmp/cel-go-0.29.patch +# 2.11.6 ships every dependency newer than the CVE pins this build used to +# force with --replace, and carries the InterpretableV2 usage natively, so the +# cel-go patch and the pin set were removed with the 2.11.6 bump. The source +# check in caddy.yml still gates the binary with govulncheck; re-add a single +# --replace here only if that goes red again. +# XCADDY_SKIP_CLEANUP keeps the build environment for that source check. RUN apk upgrade --no-cache -a && \ - apk add --no-cache ca-certificates git patch && \ + apk add --no-cache ca-certificates git && \ GOBIN=/usr/local/bin go install "github.com/caddyserver/xcaddy/cmd/xcaddy@$XCADDY_VERSION" && \ - go mod download "github.com/caddyserver/caddy/v2@$CADDY_VERSION" && \ - cp -a "$(go env GOMODCACHE)/github.com/caddyserver/caddy/v2@$CADDY_VERSION" /tmp/caddy-source && \ - chmod -R u+w /tmp/caddy-source && \ - patch -d /tmp/caddy-source -p1 < /tmp/cel-go-0.29.patch && \ - CGO_ENABLED=0 XCADDY_SETCAP=0 XCADDY_SKIP_CLEANUP=1 xcaddy build "$CADDY_VERSION" --output /usr/bin/caddy \ - --with "github.com/caddyserver/caddy/v2@$CADDY_VERSION=/tmp/caddy-source" \ - --replace "github.com/google/cel-go=github.com/google/cel-go@$CEL_GO_VERSION" \ - --replace "github.com/go-chi/chi/v5=github.com/go-chi/chi/v5@$CHI_VERSION" \ - --replace "github.com/klauspost/compress=github.com/klauspost/compress@$COMPRESS_VERSION" \ - --replace "golang.org/x/crypto=golang.org/x/crypto@$X_CRYPTO_VERSION" \ - --replace "golang.org/x/net=golang.org/x/net@$X_NET_VERSION" \ - --replace "google.golang.org/grpc=google.golang.org/grpc@$GRPC_VERSION" \ - --replace "go.opentelemetry.io/otel=go.opentelemetry.io/otel@$OTEL_VERSION" \ - --replace "go.opentelemetry.io/otel/log=go.opentelemetry.io/otel/log@$OTEL_LOG_VERSION" \ - --replace "go.opentelemetry.io/otel/sdk/log=go.opentelemetry.io/otel/sdk/log@$OTEL_LOG_VERSION" \ - --replace "go.opentelemetry.io/otel/exporters/otlp/otlptrace=go.opentelemetry.io/otel/exporters/otlp/otlptrace@$OTEL_VERSION" \ - --replace "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc=go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@$OTEL_VERSION" \ - --replace "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp=go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@$OTEL_VERSION" \ - --replace "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc=go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc@$OTEL_LOG_VERSION" \ - --replace "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp=go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@$OTEL_LOG_VERSION" \ - --replace "go.opentelemetry.io/otel/exporters/stdout/stdoutlog=go.opentelemetry.io/otel/exporters/stdout/stdoutlog@$OTEL_LOG_VERSION" && \ + CGO_ENABLED=0 XCADDY_SETCAP=0 XCADDY_SKIP_CLEANUP=1 xcaddy build "$CADDY_VERSION" --output /usr/bin/caddy && \ /usr/bin/caddy version FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 diff --git a/caddy/cel-go-0.29.patch b/caddy/cel-go-0.29.patch deleted file mode 100644 index 5e80542b9a..0000000000 --- a/caddy/cel-go-0.29.patch +++ /dev/null @@ -1,8 +0,0 @@ ---- a/modules/caddyhttp/celmatcher.go -+++ b/modules/caddyhttp/celmatcher.go -@@ -505 +505 @@ -- []interpreter.Interpretable{reqAttr}, -+ []interpreter.InterpretableV2{reqAttr}, -@@ -528 +528 @@ -- []interpreter.Interpretable{reqAttr}, -+ []interpreter.InterpretableV2{reqAttr}, diff --git a/frontend/src/hooks/useAccessList.js b/frontend/src/hooks/useAccessList.js index dbf95ceebc..9667ea8ee0 100644 --- a/frontend/src/hooks/useAccessList.js +++ b/frontend/src/hooks/useAccessList.js @@ -40,9 +40,7 @@ const useSetAccessList = () => { })); return () => queryClient.setQueryData(["access-list", values.id], previousObject); }, - onError: (_, __, rollback) => { - if (typeof rollback === "function") rollback(); - }, + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["access-list", id] }), diff --git a/frontend/src/hooks/useDeadHost.js b/frontend/src/hooks/useDeadHost.js index 5c65d75f2a..80174aae00 100644 --- a/frontend/src/hooks/useDeadHost.js +++ b/frontend/src/hooks/useDeadHost.js @@ -48,9 +48,7 @@ const useSetDeadHost = () => { })); return () => queryClient.setQueryData(["dead-host", values.id], previousObject); }, - onError: (_, __, rollback) => { - if (typeof rollback === "function") rollback(); - }, + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["dead-host", id] }), diff --git a/frontend/src/hooks/useProxyHost.js b/frontend/src/hooks/useProxyHost.js index 36666c37ef..c68539461e 100644 --- a/frontend/src/hooks/useProxyHost.js +++ b/frontend/src/hooks/useProxyHost.js @@ -63,9 +63,7 @@ const useSetProxyHost = () => { })); return () => queryClient.setQueryData(["proxy-host", values.id], previousObject); }, - onError: (_, __, rollback) => { - if (typeof rollback === "function") rollback(); - }, + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["proxy-host", id] }), diff --git a/frontend/src/hooks/useRedirectionHost.js b/frontend/src/hooks/useRedirectionHost.js index 2795967c04..f237869331 100644 --- a/frontend/src/hooks/useRedirectionHost.js +++ b/frontend/src/hooks/useRedirectionHost.js @@ -18,7 +18,7 @@ const fetchRedirectionHost = (id) => { npmplusMtlsCertificateId: 0, npmplusMtlsVerifyClientOptional: false, npmplusHttp3Support: false, - forwardScheme: "auto", + forwardScheme: "$scheme", forwardHttpCode: 301, enabled: true, hstsEnabled: false, @@ -52,9 +52,7 @@ const useSetRedirectionHost = () => { })); return () => queryClient.setQueryData(["redirection-host", values.id], previousObject); }, - onError: (_, __, rollback) => { - if (typeof rollback === "function") rollback(); - }, + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["redirection-host", id] }), diff --git a/frontend/src/hooks/useStream.js b/frontend/src/hooks/useStream.js index 1ef9f27cba..9c9ef3666d 100644 --- a/frontend/src/hooks/useStream.js +++ b/frontend/src/hooks/useStream.js @@ -48,9 +48,7 @@ const useSetStream = () => { })); return () => queryClient.setQueryData(["stream", values.id], previousObject); }, - onError: (_, __, rollback) => { - if (typeof rollback === "function") rollback(); - }, + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["stream", id] }), diff --git a/frontend/src/hooks/useUser.js b/frontend/src/hooks/useUser.js index 05fb6abe2f..21c2a133bb 100644 --- a/frontend/src/hooks/useUser.js +++ b/frontend/src/hooks/useUser.js @@ -47,9 +47,7 @@ const useSetUser = () => { })); return () => queryClient.setQueryData(["user", values.id], previousObject); }, - onError: (_, __, rollback) => { - if (typeof rollback === "function") rollback(); - }, + onError: (_, __, rollback) => rollback?.(), onSuccess: async ({ id }) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["user", id] }),