CVE-2026-40280
Description
Gotenberg is an API-based document conversion tool. In versions 8.30.1 and earlier, the default private-IP deny-lists for the --webhook-deny-list and --api-download-from-deny-list flags use a case-sensitive regular expression (^https?://) to match URL schemes. Because Go's net/url.Parse() normalizes the scheme to lowercase before establishing the outbound TCP connection, an attacker can bypass the deny-list by simply capitalizing part of the URL scheme (e.g., HTTP://, HTTPS://, or Http://). This allows unauthenticated requests to reach internal network services, including private IP ranges, loopback addresses, and cloud instance metadata endpoints such as HTTP://169.254.169.254/latest/meta-data/.
This bypasses the same security control that was patched in CVE-2026-27018.
This issue has been fixed in version 8.31.0.
Affected products
2Patches
13f01ca18d3ccfix: better denied list
5 files changed · +21 −15
compose.yaml+0 −2 modified@@ -86,8 +86,6 @@ services: - "--webhook-enable-sync-mode=${WEBHOOK_ENABLE_SYNC_MODE}" - "--webhook-allow-list=${WEBHOOK_ALLOW_LIST}" - "--webhook-deny-list=${WEBHOOK_DENY_LIST}" - - "--webhook-error-allow-list=${WEBHOOK_ERROR_ALLOW_LIST}" - - "--webhook-error-deny-list=${WEBHOOK_ERROR_DENY_LIST}" - "--webhook-max-retry=${WEBHOOK_MAX_RETRY}" - "--webhook-retry-min-wait=${WEBHOOK_RETRY_MIN_WAIT}" - "--webhook-retry-max-wait=${WEBHOOK_RETRY_MAX_WAIT}"
Makefile+2 −4 modified@@ -27,7 +27,7 @@ API_ENABLE_BASIC_AUTH=false GOTENBERG_API_BASIC_AUTH_USERNAME= GOTENBERG_API_BASIC_AUTH_PASSWORD= API_DOWNLOAD_FROM_ALLOW_LIST= -API_DOWNLOAD_FROM_DENY_LIST= +API_DOWNLOAD_FROM_DENY_LIST=^https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|127\.|localhost|\[::1\]|\[fd) API_DOWNLOAD_FROM_MAX_RETRY=4 API_DISABLE_DOWNLOAD_FROM=false API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY=true @@ -91,9 +91,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 OTEL_EXPORTER_OTLP_INSECURE=true WEBHOOK_ENABLE_SYNC_MODE=false WEBHOOK_ALLOW_LIST= -WEBHOOK_DENY_LIST= -WEBHOOK_ERROR_ALLOW_LIST= -WEBHOOK_ERROR_DENY_LIST= +WEBHOOK_DENY_LIST=^https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|127\.|localhost|\[::1\]|\[fd) WEBHOOK_MAX_RETRY=4 WEBHOOK_RETRY_MIN_WAIT=1s WEBHOOK_RETRY_MAX_WAIT=30s
pkg/modules/api/api.go+1 −1 modified@@ -196,7 +196,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor { fs.String("api-correlation-id-header", "Gotenberg-Trace", "Set the header name to use for identifying requests") fs.Bool("api-enable-basic-auth", false, "Enable basic authentication - will look for the GOTENBERG_API_BASIC_AUTH_USERNAME and GOTENBERG_API_BASIC_AUTH_PASSWORD environment variables") fs.StringSlice("api-download-from-allow-list", []string{}, "Set the allowed URLs for the download from feature using regular expressions - supports multiple values") - fs.StringSlice("api-download-from-deny-list", []string{}, "Set the denied URLs for the download from feature using regular expressions - supports multiple values") + fs.StringSlice("api-download-from-deny-list", []string{`^https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|127\.|localhost|\[::1\]|\[fd)`}, "Set the denied URLs for the download from feature using regular expressions - supports multiple values") fs.Int("api-download-from-max-retry", 4, "Set the maximum number of retries for the download from feature") fs.Bool("api-disable-download-from", false, "Disable the download from feature") fs.Bool("api-disable-health-check-route-telemetry", true, "Disable telemetry for health check route")
pkg/modules/webhook/webhook.go+14 −4 modified@@ -39,10 +39,20 @@ func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor { fs := flag.NewFlagSet("webhook", flag.ExitOnError) fs.Bool("webhook-enable-sync-mode", false, "Enable synchronous mode for the webhook feature") fs.StringSlice("webhook-allow-list", []string{}, "Set the allowed URLs for the webhook feature using regular expressions - supports multiple values") - fs.StringSlice("webhook-deny-list", []string{}, "Set the denied URLs for the webhook feature using regular expressions - supports multiple values") + fs.StringSlice("webhook-deny-list", []string{`^https?://(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|0\.0\.0\.0|127\.|localhost|\[::1\]|\[fd)`}, "Set the denied URLs for the webhook feature using regular expressions - supports multiple values") + fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature") + + // Deprecated flags. fs.StringSlice("webhook-error-allow-list", []string{}, "Set the allowed URLs in case of an error for the webhook feature using regular expressions - supports multiple values") fs.StringSlice("webhook-error-deny-list", []string{}, "Set the denied URLs in case of an error for the webhook feature using regular expressions - supports multiple values") - fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature") + err := fs.MarkDeprecated("webhook-error-allow-list", "use --webhook-allow-list instead") + if err != nil { + panic(err) + } + err = fs.MarkDeprecated("webhook-error-deny-list", "use --webhook-deny-list instead") + if err != nil { + panic(err) + } fs.Duration("webhook-retry-min-wait", time.Duration(1)*time.Second, "Set the minimum duration to wait before trying to call the webhook again") fs.Duration("webhook-retry-max-wait", time.Duration(30)*time.Second, "Set the maximum duration to wait before trying to call the webhook again") fs.Duration("webhook-client-timeout", time.Duration(30)*time.Second, "Set the time limit for requests to the webhook") @@ -60,8 +70,8 @@ func (w *Webhook) Provision(ctx *gotenberg.Context) error { w.enableSyncMode = flags.MustBool("webhook-enable-sync-mode") w.allowList = flags.MustRegexpSlice("webhook-allow-list") w.denyList = flags.MustRegexpSlice("webhook-deny-list") - w.errorAllowList = flags.MustRegexpSlice("webhook-error-allow-list") - w.errorDenyList = flags.MustRegexpSlice("webhook-error-deny-list") + w.errorAllowList = flags.MustDeprecatedRegexpSlice("webhook-error-allow-list", "webhook-allow-list") + w.errorDenyList = flags.MustDeprecatedRegexpSlice("webhook-error-deny-list", "webhook-deny-list") w.maxRetry = flags.MustInt("webhook-max-retry") w.retryMinWait = flags.MustDuration("webhook-retry-min-wait") w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")
test/integration/features/debug.feature+4 −4 modified@@ -63,7 +63,7 @@ Feature: /debug "api-disable-root-route-telemetry": "true", "api-disable-version-route-telemetry": "true", "api-download-from-allow-list": "[]", - "api-download-from-deny-list": "[]", + "api-download-from-deny-list": "[^https?://(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|169\\.254\\.|0\\.0\\.0\\.0|127\\.|localhost|\\[::1\\]|\\[fd)]", "api-download-from-max-retry": "4", "api-enable-basic-auth": "false", "api-enable-debug-route": "true", @@ -126,7 +126,7 @@ Feature: /debug "prometheus-metrics-path": "/prometheus/metrics", "webhook-allow-list": "[]", "webhook-client-timeout": "30s", - "webhook-deny-list": "[]", + "webhook-deny-list": "[^https?://(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|169\\.254\\.|0\\.0\\.0\\.0|127\\.|localhost|\\[::1\\]|\\[fd)]", "webhook-disable": "false", "webhook-error-allow-list": "[]", "webhook-error-deny-list": "[]", @@ -195,7 +195,7 @@ Feature: /debug "api-disable-root-route-telemetry": "true", "api-disable-version-route-telemetry": "true", "api-download-from-allow-list": "[]", - "api-download-from-deny-list": "[]", + "api-download-from-deny-list": "[^https?://(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|169\\.254\\.|0\\.0\\.0\\.0|127\\.|localhost|\\[::1\\]|\\[fd)]", "api-download-from-max-retry": "4", "api-enable-basic-auth": "false", "api-enable-debug-route": "true", @@ -258,7 +258,7 @@ Feature: /debug "prometheus-metrics-path": "/prometheus/metrics", "webhook-allow-list": "[]", "webhook-client-timeout": "30s", - "webhook-deny-list": "[]", + "webhook-deny-list": "[^https?://(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|169\\.254\\.|0\\.0\\.0\\.0|127\\.|localhost|\\[::1\\]|\\[fd)]", "webhook-disable": "false", "webhook-error-allow-list": "[]", "webhook-error-deny-list": "[]",
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
6- github.com/gotenberg/gotenberg/commit/3f01ca18d3cc21375a1e2da4b5a3f261c8548e47nvdPatch
- github.com/gotenberg/gotenberg/security/advisories/GHSA-5q7p-7jgv-ww56nvdExploitMitigationVendor Advisory
- github.com/advisories/GHSA-5q7p-7jgv-ww56ghsaADVISORY
- github.com/advisories/GHSA-jjwv-57xh-xr6rnvdNot Applicable
- github.com/gotenberg/gotenberg/releases/tag/v8.31.0ghsa
- nvd.nist.gov/vuln/detail/CVE-2026-40280ghsa
News mentions
0No linked articles in our index yet.