VYPR
High severity7.5GHSA Advisory· Published May 5, 2026· Updated May 8, 2026

CVE-2026-40280

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

2

Patches

1
3f01ca18d3cc

fix: better denied list

https://github.com/gotenberg/gotenbergJulien NeuhartApr 7, 2026via nvd-ref
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

News mentions

0

No linked articles in our index yet.