mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-07 21:13:18 +01:00
docs(outbound): document the allow-list bypass on every allow-list flag and terminate the example patterns
This commit is contained in:
8
Makefile
8
Makefile
@@ -33,7 +33,10 @@ API_OIDC_ISSUER=
|
|||||||
API_OIDC_AUDIENCE=
|
API_OIDC_AUDIENCE=
|
||||||
API_OIDC_JWKS_URL=
|
API_OIDC_JWKS_URL=
|
||||||
API_DOWNLOAD_FROM_ALLOW_LIST=
|
API_DOWNLOAD_FROM_ALLOW_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)
|
# Empty, like the flag default since 8.32.0. A textual deny-list cannot
|
||||||
|
# enumerate every way to write a private address, so *_DENY_PRIVATE_IPS is the
|
||||||
|
# control to reach for. Left false here so local testing can reach the host.
|
||||||
|
API_DOWNLOAD_FROM_DENY_LIST=
|
||||||
API_DOWNLOAD_FROM_DENY_PRIVATE_IPS=false
|
API_DOWNLOAD_FROM_DENY_PRIVATE_IPS=false
|
||||||
API_DOWNLOAD_FROM_DENY_PUBLIC_IPS=false
|
API_DOWNLOAD_FROM_DENY_PUBLIC_IPS=false
|
||||||
API_DOWNLOAD_FROM_ENABLE_ENVIRONMENT_PROXY=false
|
API_DOWNLOAD_FROM_ENABLE_ENVIRONMENT_PROXY=false
|
||||||
@@ -114,7 +117,8 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|||||||
OTEL_EXPORTER_OTLP_INSECURE=true
|
OTEL_EXPORTER_OTLP_INSECURE=true
|
||||||
WEBHOOK_ENABLE_SYNC_MODE=false
|
WEBHOOK_ENABLE_SYNC_MODE=false
|
||||||
WEBHOOK_ALLOW_LIST=
|
WEBHOOK_ALLOW_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)
|
# See the note on API_DOWNLOAD_FROM_DENY_LIST.
|
||||||
|
WEBHOOK_DENY_LIST=
|
||||||
WEBHOOK_DENY_PRIVATE_IPS=false
|
WEBHOOK_DENY_PRIVATE_IPS=false
|
||||||
WEBHOOK_DENY_PUBLIC_IPS=false
|
WEBHOOK_DENY_PUBLIC_IPS=false
|
||||||
WEBHOOK_ENABLE_ENVIRONMENT_PROXY=false
|
WEBHOOK_ENABLE_ENVIRONMENT_PROXY=false
|
||||||
|
|||||||
@@ -343,8 +343,9 @@ const (
|
|||||||
const hostTerminators = "/:#?"
|
const hostTerminators = "/:#?"
|
||||||
|
|
||||||
// crosserClassChars are the characters that, if a class can match them, let a
|
// crosserClassChars are the characters that, if a class can match them, let a
|
||||||
// match escape the authority.
|
// match escape the authority. "/" is deliberately absent: it ends the
|
||||||
const crosserClassChars = "/@?#"
|
// authority rather than escaping it, so a class such as "[:/]" is safe.
|
||||||
|
const crosserClassChars = "@?#"
|
||||||
|
|
||||||
// classifyHostToken classifies one token of the authority walk.
|
// classifyHostToken classifies one token of the authority walk.
|
||||||
func classifyHostToken(token string) hostTokenKind {
|
func classifyHostToken(token string) hostTokenKind {
|
||||||
@@ -383,6 +384,9 @@ func classifyHostToken(token string) hostTokenKind {
|
|||||||
if classContainsAny(inner, crosserClassChars) {
|
if classContainsAny(inner, crosserClassChars) {
|
||||||
return hostTokenCrosser
|
return hostTokenCrosser
|
||||||
}
|
}
|
||||||
|
if classOnlyTerminators(inner) {
|
||||||
|
return hostTokenTerminator
|
||||||
|
}
|
||||||
return hostTokenNeutral
|
return hostTokenNeutral
|
||||||
|
|
||||||
case strings.HasPrefix(token, "("):
|
case strings.HasPrefix(token, "("):
|
||||||
@@ -477,6 +481,35 @@ func tokenAt(s string, i int) string {
|
|||||||
return s[i : i+1]
|
return s[i : i+1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// classOnlyTerminators reports whether every character a class can match ends
|
||||||
|
// the authority, which makes the class itself a terminator. A range is never
|
||||||
|
// treated as one.
|
||||||
|
func classOnlyTerminators(class string) bool {
|
||||||
|
if class == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(class); i++ {
|
||||||
|
if class[i] == '\\' && i+1 < len(class) {
|
||||||
|
if !strings.Contains(hostTerminators, class[i+1:i+2]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i+2 < len(class) && class[i+1] == '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(hostTerminators, class[i:i+1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// classContainsAny reports whether a character class body can match any of the
|
// classContainsAny reports whether a character class body can match any of the
|
||||||
// given characters, expanding simple ranges.
|
// given characters, expanding simple ranges.
|
||||||
func classContainsAny(class, chars string) bool {
|
func classContainsAny(class, chars string) bool {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ func TestAuditAllowList(t *testing.T) {
|
|||||||
{"alternation both anchored and terminated", `^https://a\.example/|^https://b\.example/`, ""},
|
{"alternation both anchored and terminated", `^https://a\.example/|^https://b\.example/`, ""},
|
||||||
{"no authority to check", `^file:///tmp/`, ""},
|
{"no authority to check", `^file:///tmp/`, ""},
|
||||||
{"digit class in host", `^https://node\d+\.example\.com/`, ""},
|
{"digit class in host", `^https://node\d+\.example\.com/`, ""},
|
||||||
|
{"class of only terminators", `^https://example\.com[:/]`, ""},
|
||||||
|
{"feature file pattern, fixed", `^https?://host\.docker\.internal(:[0-9]+)?/`, ""},
|
||||||
|
|
||||||
// Unanchored: regexp2 searches, so these match anywhere in the URL.
|
// Unanchored: regexp2 searches, so these match anywhere in the URL.
|
||||||
{"no anchor", `trusted\.example\.com`, AllowListRiskUnanchored},
|
{"no anchor", `trusted\.example\.com`, AllowListRiskUnanchored},
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.String("api-oidc-issuer", "", "Set the OIDC issuer URL, e.g. https://tenant.example.com/ - the token 'iss' claim must match")
|
fs.String("api-oidc-issuer", "", "Set the OIDC issuer URL, e.g. https://tenant.example.com/ - the token 'iss' claim must match")
|
||||||
fs.String("api-oidc-audience", "", "Set the expected OIDC audience - the token 'aud' claim must contain it")
|
fs.String("api-oidc-audience", "", "Set the expected OIDC audience - the token 'aud' claim must contain it")
|
||||||
fs.String("api-oidc-jwks-url", "", "Set the OIDC JWKS URL - discovered from the issuer's well-known configuration when empty")
|
fs.String("api-oidc-jwks-url", "", "Set the OIDC JWKS URL - discovered from the issuer's well-known configuration when empty")
|
||||||
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-allow-list", []string{}, `Set the allowed URLs for the download from feature using regular expressions - supports multiple values. A match bypasses --api-download-from-deny-private-ips (API_DOWNLOAD_FROM_DENY_PRIVATE_IPS) and --api-download-from-deny-public-ips (API_DOWNLOAD_FROM_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`)
|
||||||
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{}, "Set the denied URLs for the download from feature using regular expressions - supports multiple values")
|
||||||
fs.Bool("api-download-from-deny-private-ips", false, "Reject downloadFrom URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted downloadFrom sources to mitigate SSRF against internal services")
|
fs.Bool("api-download-from-deny-private-ips", false, "Reject downloadFrom URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted downloadFrom sources to mitigate SSRF against internal services")
|
||||||
fs.Bool("api-download-from-deny-public-ips", false, "Reject downloadFrom URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent downloads from reaching the public internet")
|
fs.Bool("api-download-from-deny-public-ips", false, "Reject downloadFrom URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent downloads from reaching the public internet")
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver")
|
fs.String("chromium-host-resolver-rules", "", "Set custom mappings to the host resolver")
|
||||||
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
fs.String("chromium-proxy-server", "", "Set the outbound proxy server; this switch only affects HTTP and HTTPS requests")
|
||||||
fs.Bool("chromium-enable-environment-proxy", false, "Route Chromium's outbound requests through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials. Use this instead of --chromium-proxy-server for authenticated proxies, and leave --chromium-proxy-server and --chromium-host-resolver-rules unset")
|
fs.Bool("chromium-enable-environment-proxy", false, "Route Chromium's outbound requests through the proxy defined by the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY variables, including credentials. Use this instead of --chromium-proxy-server for authenticated proxies, and leave --chromium-proxy-server and --chromium-host-resolver-rules unset")
|
||||||
fs.StringSlice("chromium-allow-list", []string{}, "Set the allowed URLs for Chromium using regular expressions - supports multiple values")
|
fs.StringSlice("chromium-allow-list", []string{}, `Set the allowed URLs for Chromium using regular expressions - supports multiple values. A match bypasses --chromium-deny-private-ips (CHROMIUM_DENY_PRIVATE_IPS) and --chromium-deny-public-ips (CHROMIUM_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`)
|
||||||
fs.StringSlice("chromium-deny-list", []string{`^file:(?!//\/tmp/).*`}, "Set the denied URLs for Chromium using regular expressions - supports multiple values")
|
fs.StringSlice("chromium-deny-list", []string{`^file:(?!//\/tmp/).*`}, "Set the denied URLs for Chromium using regular expressions - supports multiple values")
|
||||||
fs.Bool("chromium-deny-private-ips", false, "Reject URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted form input to mitigate SSRF against internal services")
|
fs.Bool("chromium-deny-private-ips", false, "Reject URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted form input to mitigate SSRF against internal services")
|
||||||
fs.Bool("chromium-deny-public-ips", false, "Reject URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
|
fs.Bool("chromium-deny-public-ips", false, "Reject URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.Duration("libreoffice-idle-shutdown-timeout", 0, "Shutdown LibreOffice after being idle for the given duration. Set to 0 to disable this feature")
|
fs.Duration("libreoffice-idle-shutdown-timeout", 0, "Shutdown LibreOffice after being idle for the given duration. Set to 0 to disable this feature")
|
||||||
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
|
fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion")
|
||||||
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart")
|
||||||
fs.StringSlice("libreoffice-allow-list", []string{}, "Set the allowed URLs for LibreOffice outbound fetches (embedded images, linked content) using regular expressions - supports multiple values")
|
fs.StringSlice("libreoffice-allow-list", []string{}, `Set the allowed URLs for LibreOffice outbound fetches (embedded images, linked content) using regular expressions - supports multiple values. A match bypasses --libreoffice-deny-private-ips (LIBREOFFICE_DENY_PRIVATE_IPS) and --libreoffice-deny-public-ips (LIBREOFFICE_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`)
|
||||||
fs.StringSlice("libreoffice-deny-list", []string{}, "Set the denied URLs for LibreOffice outbound fetches using regular expressions - supports multiple values")
|
fs.StringSlice("libreoffice-deny-list", []string{}, "Set the denied URLs for LibreOffice outbound fetches using regular expressions - supports multiple values")
|
||||||
fs.Bool("libreoffice-deny-private-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted documents to mitigate SSRF against internal services")
|
fs.Bool("libreoffice-deny-private-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted documents to mitigate SSRF against internal services")
|
||||||
fs.Bool("libreoffice-deny-public-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
|
fs.Bool("libreoffice-deny-public-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network")
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
FlagSet: func() *flag.FlagSet {
|
FlagSet: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("webhook", flag.ExitOnError)
|
fs := flag.NewFlagSet("webhook", flag.ExitOnError)
|
||||||
fs.Bool("webhook-enable-sync-mode", false, "Enable synchronous mode for the webhook feature")
|
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-allow-list", []string{}, `Set the allowed URLs for the webhook feature using regular expressions - supports multiple values. A match bypasses --webhook-deny-private-ips (WEBHOOK_DENY_PRIVATE_IPS) and --webhook-deny-public-ips (WEBHOOK_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`)
|
||||||
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{}, "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-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.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")
|
||||||
@@ -50,7 +50,7 @@ func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
|
fs.Int("webhook-max-retry", 4, "Set the maximum number of retries for the webhook feature")
|
||||||
|
|
||||||
// Deprecated flags.
|
// 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-allow-list", []string{}, `Set the allowed URLs in case of an error for the webhook feature using regular expressions - supports multiple values. A match bypasses --webhook-deny-private-ips (WEBHOOK_DENY_PRIVATE_IPS) and --webhook-deny-public-ips (WEBHOOK_DENY_PUBLIC_IPS), so terminate the host or the pattern also matches suffix hosts, for example ^https?://internal\.svc(:|/|$)`)
|
||||||
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.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")
|
||||||
err := fs.MarkDeprecated("webhook-error-allow-list", "use --webhook-allow-list instead")
|
err := fs.MarkDeprecated("webhook-error-allow-list", "use --webhook-allow-list instead")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -589,7 +589,7 @@ Feature: /forms/chromium/convert/url
|
|||||||
@chromium-ssrf
|
@chromium-ssrf
|
||||||
Scenario: POST /forms/chromium/convert/url (Redirect to a non-allow-listed address is re-filtered)
|
Scenario: POST /forms/chromium/convert/url (Redirect to a non-allow-listed address is re-filtered)
|
||||||
Given I have a Gotenberg container with the following environment variable(s):
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
| CHROMIUM_ALLOW_LIST | ^https?://host.docker.internal.* |
|
| CHROMIUM_ALLOW_LIST | ^https?://host\.docker\.internal(:[0-9]+)?/ |
|
||||||
Given I have a static server
|
Given I have a static server
|
||||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||||
| url | http://host.docker.internal:%d/redirect-to-private | field |
|
| url | http://host.docker.internal:%d/redirect-to-private | field |
|
||||||
|
|||||||
Reference in New Issue
Block a user