fix(outbound)!: per-module deny-private-ips and deny-public-ips, permissive defaults

This commit is contained in:
Julien Neuhart
2026-04-23 20:01:27 +02:00
parent a2a8c42457
commit 7a914fce65
13 changed files with 365 additions and 162 deletions

View File

@@ -127,15 +127,19 @@ func webhookMiddleware(w *Webhook) api.Middleware {
}
// Let's check if the webhook URLs are acceptable according to our
// allowed/denied lists, and against the IP-based outbound URL
// guard. See [gotenberg.FilterOutboundURL].
err := gotenberg.FilterOutboundURL(ctx, webhookUrl, w.allowList, w.denyList, deadline)
// allowed/denied lists, and against the IP-class options.
// See [gotenberg.FilterOutboundURL].
ipOpts := []gotenberg.DecideOption{
gotenberg.WithDenyPrivateIPs(w.denyPrivateIPs),
gotenberg.WithDenyPublicIPs(w.denyPublicIPs),
}
err := gotenberg.FilterOutboundURL(ctx, webhookUrl, w.allowList, w.denyList, deadline, ipOpts...)
if err != nil {
return fmt.Errorf("filter webhook URL: %w", err)
}
if webhookErrorUrl != "" {
err = gotenberg.FilterOutboundURL(ctx, webhookErrorUrl, w.errorAllowList, w.errorDenyList, deadline)
err = gotenberg.FilterOutboundURL(ctx, webhookErrorUrl, w.errorAllowList, w.errorDenyList, deadline, ipOpts...)
if err != nil {
return fmt.Errorf("filter webhook error URL: %w", err)
}
@@ -198,7 +202,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
// Filter the events URL if provided.
if webhookEventsUrl != "" {
err = gotenberg.FilterOutboundURL(ctx, webhookEventsUrl, w.allowList, w.denyList, deadline)
err = gotenberg.FilterOutboundURL(ctx, webhookEventsUrl, w.allowList, w.denyList, deadline, ipOpts...)
if err != nil {
return fmt.Errorf("filter webhook events URL: %w", err)
}
@@ -220,7 +224,7 @@ func webhookMiddleware(w *Webhook) api.Middleware {
startTime: startTime,
client: &retryablehttp.Client{
HTTPClient: gotenberg.NewOutboundHttpClient(w.clientTimeout, w.allowList, w.denyList),
HTTPClient: gotenberg.NewOutboundHttpClient(w.clientTimeout, w.allowList, w.denyList, ipOpts...),
RetryMax: w.maxRetry,
RetryWaitMin: w.retryMinWait,
RetryWaitMax: w.retryMaxWait,

View File

@@ -23,6 +23,8 @@ type Webhook struct {
denyList []*regexp2.Regexp
errorAllowList []*regexp2.Regexp
errorDenyList []*regexp2.Regexp
denyPrivateIPs bool
denyPublicIPs bool
maxRetry int
retryMinWait time.Duration
retryMaxWait time.Duration
@@ -39,7 +41,9 @@ 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{`^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.StringSlice("webhook-deny-list", []string{}, "Set the denied URLs for the webhook feature using regular expressions - supports multiple values")
fs.Bool("webhook-deny-private-ips", false, "Reject webhook URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted webhook destinations to mitigate SSRF against internal services")
fs.Bool("webhook-deny-public-ips", false, "Reject webhook URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent callbacks from leaving a private network")
fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
// Deprecated flags.
@@ -72,6 +76,8 @@ func (w *Webhook) Provision(ctx *gotenberg.Context) error {
w.denyList = flags.MustRegexpSlice("webhook-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.denyPrivateIPs = flags.MustBool("webhook-deny-private-ips")
w.denyPublicIPs = flags.MustBool("webhook-deny-public-ips")
w.maxRetry = flags.MustInt("webhook-max-retry")
w.retryMinWait = flags.MustDuration("webhook-retry-min-wait")
w.retryMaxWait = flags.MustDuration("webhook-retry-max-wait")