CVE-2024-21527 (Go, published 2024-07-19T05:15Z)
Description: value: Versions of the package github.com/gotenberg/gotenberg/v8/pkg/gotenberg before 8.1.0; versions of the package github.com/gotenberg/gotenberg/v8/pkg/modules/chromium before 8.1.0; versions of the package github.com/gotenberg/gotenberg/v8/pkg/modules/webhook before 8.1.0 are vulnerable to Server-side Request Forgery (SSRF) via the /convert/html endpoint when a request is made to a file via localhost, such as <iframe src="\\localhost/etc/passwd">. By exploiting this vulnerability, an attacker can achieve local file inclusion, allowing of sensitive files read on the host system. WorkaroundAn alternative is using either or both --chromium-deny-list and --chromium-allow-list flags.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/gotenberg/gotenberg
Files changed:
Makefile
pkg/gotenberg/flags.go
pkg/gotenberg/regexp.go
pkg/gotenberg/regexp_test.go
pkg/gotenberg/supervisor_test.go
pkg/modules/chromium/browser.go
pkg/modules/chromium/browser_test.go
pkg/modules/chromium/chromium.go
pkg/modules/chromium/events.go
pkg/modules/chromium/routes.go
pkg/modules/chromium/routes_test.go
pkg/modules/webhook/middleware.go
pkg/modules/webhook/middleware_test.go
pkg/modules/webhook/webhook.go
Commit message: fix(chromium): better default deny list regexp


Vulnerable code:
CHROMIUM_DENY_LIST="^file:///[^tmp].*"
	--chromium-allow-list=$(CHROMIUM_ALLOW_LIST) \
	--chromium-deny-list=$(CHROMIUM_DENY_LIST) \
	--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \
	--webhook-deny-list=$(WEBHOOK_DENY_LIST) \
	"regexp"
func (f *ParsedFlags) MustRegexp(name string) *regexp.Regexp {
	return regexp.MustCompile(val)
func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp.Regexp {
			defer cancel()

	"regexp"
	allowList         *regexp.Regexp
	denyList          *regexp.Regexp
	// We validate the "main" URL against our allow / deny lists.
	if !b.arguments.allowList.MatchString(url) {
		return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", url, ErrUrlNotAuthorized)
	}

	if b.arguments.denyList.String() != "" && b.arguments.denyList.MatchString(url) {
		return fmt.Errorf("'%s' matches the expression from the denied list: %w", url, ErrUrlNotAuthorized)
	}

	err := chromedp.Run(taskCtx, tasks...)
	"regexp"
			scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list",
				b.arguments = browserArguments{
					allowList: regexp.MustCompile("^file:///[^tmp].*"),
				}
			fs:            gotenberg.NewFileSystem(),
			noDeadline:    false,
			start:         false,
			expectError:   true,
			expectedError: ErrUrlNotAuthorized,
			scenario: "ErrUrlNotAuthorized: main URL does match the denied list",
					allowList: regexp.MustCompile(""),
					denyList:  regexp.MustCompile("^file:///tmp.*"),
			expectedError: ErrUrlNotAuthorized,
			scenario: "ErrUrlNotAuthorized: main URL does match the denied list",
					allowList: regexp.MustCompile(""),
					denyList:  regexp.MustCompile(""),
			fs:          gotenberg.NewFileSystem(),
			noDeadline:  true,
			start:       false,
			expectError: true,
					allowList:        regexp.MustCompile("^file:///tmp.*"),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile("^file:///[^tmp].*"),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:         regexp.MustCompile(""),
					denyList:          regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
			scenario: "ErrUrlNotAuthorized: main URL does not match the allowed list",
					allowList: regexp.MustCompile("^file:///[^tmp].*"),
			fs:            gotenberg.NewFileSystem(),
			noDeadline:    false,
			start:         false,
			expectError:   true,
			expectedError: ErrUrlNotAuthorized,
			scenario: "ErrUrlNotAuthorized: main URL does match the denied list",
					allowList: regexp.MustCompile(""),
					denyList:  regexp.MustCompile("^file:///tmp.*"),
			expectedError: ErrUrlNotAuthorized,
			scenario: "ErrUrlNotAuthorized: main URL does match the denied list",
					allowList: regexp.MustCompile(""),
					denyList:  regexp.MustCompile(""),
			fs:          gotenberg.NewFileSystem(),
			noDeadline:  true,
			start:       false,
			expectError: true,
					allowList:        regexp.MustCompile("^file:///tmp.*"),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile("^file:///[^tmp].*"),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:         regexp.MustCompile(""),
					denyList:          regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
					allowList:        regexp.MustCompile(""),
					denyList:         regexp.MustCompile(""),
	// ErrUrlNotAuthorized happens if a URL is not acceptable according to the
	// allowed/denied lists.
	ErrUrlNotAuthorized = errors.New("URL not authorized")

			fs.String("chromium-deny-list", "^file:///[^tmp].*", "Set the denied URLs for Chromium using a regular expression")
	"regexp"
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp.Regexp, denyList *regexp.Regexp) {
				if !allowList.MatchString(e.Request.URL) {
					logger.Warn(fmt.Sprintf("'%s' does not match the expression from the allowed list", e.Request.URL))
					allow = false
				if denyList.String() != "" && denyList.MatchString(e.Request.URL) {
					logger.Warn(fmt.Sprintf("'%s' matches the expression from the denied list", e.Request.URL))
					err := req.Do(executorCtx)

				err := req.Do(executorCtx)
	if errors.Is(err, ErrUrlNotAuthorized) {
			scenario: "ErrUrlNotAuthorized",
				return ErrUrlNotAuthorized
			scenario: "ErrUrlNotAuthorized",
				return ErrUrlNotAuthorized
	"regexp"
					filter := func(URL, header string, allowList, denyList *regexp.Regexp) error {
						if !allowList.MatchString(URL) {
							return api.WrapError(
								fmt.Errorf("'%s' does not match the expression from the allowed list", URL),
								api.NewSentinelHttpError(
									http.StatusForbidden,
									fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL),
								),
							)
						if denyList.String() != "" && denyList.MatchString(URL) {
								fmt.Errorf("'%s' matches the expression from the denied list", URL),
									fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URLs", header, URL),
						return nil
					err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList)
					err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList)
	"regexp"
			allowList:      regexp.MustCompile(""),
			denyList:       regexp.MustCompile(""),
			errorAllowList: regexp.MustCompile(""),
			errorDenyList:  regexp.MustCompile(""),
				mod.allowList = regexp.MustCompile("bar")
				mod.denyList = regexp.MustCompile("foo")
				mod.errorAllowList = regexp.MustCompile("foo")
				mod.errorDenyList = regexp.MustCompile("bar")
			ctx := &api.ContextMock{Context: &api.Context{}}
			ctx.SetEchoContext(c)

			c.Set("context", ctx.Context)
			c.Set("cancel", func() context.CancelFunc {
				return func() {
					return
				}
			}())
			allowList:      regexp.MustCompile(""),
			denyList:       regexp.MustCompile(""),
			errorAllowList: regexp.MustCompile(""),
			errorDenyList:  regexp.MustCompile(""),
			ctx := &api.ContextMock{Context: &api.Context{}}
			c.Set("cancel", func() context.CancelFunc {
				return func() {
					return
				}
			}())

			rand.Seed(time.Now().UnixNano())
	"regexp"
	allowList      *regexp.Regexp
	denyList       *regexp.Regexp
	errorAllowList *regexp.Regexp
	errorDenyList  *regexp.Regexp

Fixed code:
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
	--chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \
	--chromium-deny-list="$(CHROMIUM_DENY_LIST)" \
	--webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
	--webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
	"github.com/dlclark/regexp2"
func (f *ParsedFlags) MustRegexp(name string) *regexp2.Regexp {
	return regexp2.MustCompile(val, 0)
func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp2.Regexp {
package gotenberg

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/dlclark/regexp2"
)

// ErrFiltered happens if a value is filtered by the [FilterDeadline] function.
var ErrFiltered = errors.New("value filtered")

// FilterDeadline checks if given value is allowed and not denied according to
// regex patterns. It returns a [context.DeadlineExceeded] if it takes too long
// to process.
func FilterDeadline(allowed, denied *regexp2.Regexp, s string, deadline time.Time) error {
	// FIXME: not ideal to compile everytime, but is there another way to create a clone?
	if allowed.String() != "" {
		allow := regexp2.MustCompile(allowed.String(), 0)
		allow.MatchTimeout = time.Until(deadline)

		ok, err := allow.MatchString(s)
		if err != nil {
			if time.Now().After(deadline) {
				return context.DeadlineExceeded
			}
			return fmt.Errorf("'%s' cannot handle '%s': %w", allow.String(), s, err)
		}
		if !ok {
			return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", s, ErrFiltered)
		}
	}

	if denied.String() != "" {
		deny := regexp2.MustCompile(denied.String(), 0)
		deny.MatchTimeout = time.Until(deadline)

		ok, err := deny.MatchString(s)
		if err != nil {
			if time.Now().After(deadline) {
				return context.DeadlineExceeded
			}
			return fmt.Errorf("'%s' cannot handle '%s': %w", deny.String(), s, err)
		}
		if ok {
			return fmt.Errorf("'%s' matches the expression from the denied list: %w", s, ErrFiltered)
		}
	}

	return nil
}
package gotenberg

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/dlclark/regexp2"
)

func TestFilterDeadline(t *testing.T) {
	for _, tc := range []struct {
		scenario      string
		allowed       *regexp2.Regexp
		denied        *regexp2.Regexp
		s             string
		deadline      time.Time
		expectError   bool
		expectedError error
	}{
		{
			scenario:      "DeadlineExceeded (allowed)",
			allowed:       regexp2.MustCompile("foo", 0),
			denied:        regexp2.MustCompile("", 0),
			s:             "foo",
			deadline:      time.Now().Add(time.Duration(-1) * time.Hour),
			expectError:   true,
			expectedError: context.DeadlineExceeded,
		},
		{
			scenario:      "ErrFiltered (allowed)",
			allowed:       regexp2.MustCompile("foo", 0),
			denied:        regexp2.MustCompile("", 0),
			s:             "bar",
			deadline:      time.Now().Add(time.Duration(5) * time.Second),
			expectError:   true,
			expectedError: ErrFiltered,
		},
		{
			scenario:      "DeadlineExceeded (denied)",
			allowed:       regexp2.MustCompile("", 0),
			denied:        regexp2.MustCompile("foo", 0),
			s:             "foo",
			deadline:      time.Now().Add(time.Duration(-1) * time.Hour),
			expectError:   true,
			expectedError: context.DeadlineExceeded,
		},
		{
			scenario:      "ErrFiltered (denied)",
			allowed:       regexp2.MustCompile("", 0),
			denied:        regexp2.MustCompile("foo", 0),
			s:             "foo",
			deadline:      time.Now().Add(time.Duration(5) * time.Second),
			expectError:   true,
			expectedError: ErrFiltered,
		},
		{
			scenario:    "success",
			allowed:     regexp2.MustCompile("", 0),
			denied:      regexp2.MustCompile("", 0),
			s:           "foo",
			deadline:    time.Now().Add(time.Duration(5) * time.Second),
			expectError: false,
		},
	} {
		t.Run(tc.scenario, func(t *testing.T) {
			err := FilterDeadline(tc.allowed, tc.denied, tc.s, tc.deadline)

			if tc.expectError && err == nil {
				t.Fatal("expected an error but got none")
			}

			if !tc.expectError && err != nil {
				t.Fatalf("expected no error but got: %v", err)
			}

			if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
				t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
			}
		})
	}
}
			} else {
				defer cancel()
	"github.com/dlclark/regexp2"
	allowList         *regexp2.Regexp
	denyList          *regexp2.Regexp
	// We validate the "main" URL against our allow / deny lists.
	err := gotenberg.FilterDeadline(b.arguments.allowList, b.arguments.denyList, url, deadline)
	if err != nil {
		return fmt.Errorf("filter URL: %w", err)
	}

	err = chromedp.Run(taskCtx, tasks...)
	"github.com/dlclark/regexp2"
			scenario: "context has no deadline",
			fs:          gotenberg.NewFileSystem(),
			noDeadline:  true,
			start:       false,
			expectError: true,
			scenario: "ErrFiltered: main URL does not match the allowed list",
					allowList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0),
					denyList:  regexp2.MustCompile("", 0),
			expectedError: gotenberg.ErrFiltered,
			scenario: "ErrFiltered: main URL does match the denied list",
					allowList: regexp2.MustCompile("", 0),
					denyList:  regexp2.MustCompile("^file:///tmp.*", 0),
			fs:            gotenberg.NewFileSystem(),
			noDeadline:    false,
			start:         false,
			expectError:   true,
			expectedError: gotenberg.ErrFiltered,
					allowList:        regexp2.MustCompile("^file:///tmp.*", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:         regexp2.MustCompile("", 0),
					denyList:          regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
			scenario: "context has not deadline",
					allowList: regexp2.MustCompile("", 0),
					denyList:  regexp2.MustCompile("", 0),
			fs:          gotenberg.NewFileSystem(),
			noDeadline:  true,
			start:       false,
			expectError: true,
			scenario: "ErrFiltered: main URL does not match the allowed list",
					allowList: regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0),
					denyList:  regexp2.MustCompile("", 0),
			expectedError: gotenberg.ErrFiltered,
			scenario: "ErrFiltered: main URL does match the denied list",
					allowList: regexp2.MustCompile("", 0),
					denyList:  regexp2.MustCompile("^file:///tmp.*", 0),
			fs:            gotenberg.NewFileSystem(),
			noDeadline:    false,
			start:         false,
			expectError:   true,
			expectedError: gotenberg.ErrFiltered,
					allowList:        regexp2.MustCompile("^file:///tmp.*", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile(`^file:(?!//\/tmp/).*`, 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:         regexp2.MustCompile("", 0),
					denyList:          regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
					allowList:        regexp2.MustCompile("", 0),
					denyList:         regexp2.MustCompile("", 0),
			fs.String("chromium-deny-list", `^file:(?!//\/tmp/).*`, "Set the denied URLs for Chromium using a regular expression")
	"github.com/dlclark/regexp2"

	"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger, allowList *regexp2.Regexp, denyList *regexp2.Regexp) {
				deadline, ok := ctx.Deadline()
				if !ok {
					logger.Error("context has no deadline, cannot filter URL")
					return
				err := gotenberg.FilterDeadline(allowList, denyList, e.Request.URL, deadline)
				if err != nil {
					logger.Warn(err.Error())
					err = req.Do(executorCtx)
				err = req.Do(executorCtx)
	if errors.Is(err, gotenberg.ErrFiltered) {
			scenario: "ErrFiltered",
				return gotenberg.ErrFiltered
			scenario: "ErrFiltered",
				return gotenberg.ErrFiltered
	"github.com/dlclark/regexp2"
	"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
					deadline, ok := ctx.Deadline()
					if !ok {
						return errors.New("context has no deadline")
					}

					filter := func(url, header string, allowList, denyList *regexp2.Regexp, deadline time.Time) error {
						err := gotenberg.FilterDeadline(allowList, denyList, url, deadline)
						if err == nil {
							return nil
						if errors.Is(err, gotenberg.ErrFiltered) {
								err,
									fmt.Sprintf("Invalid '%s' header value: '%s' does not match the authorized URL", header, url),
						return err
					err := filter(webhookUrl, "Gotenberg-Webhook-Url", w.allowList, w.denyList, deadline)
					err = filter(webhookErrorUrl, "Gotenberg-Webhook-Error-Url", w.errorAllowList, w.errorDenyList, deadline)
	"github.com/dlclark/regexp2"
			allowList:      regexp2.MustCompile("", 0),
			denyList:       regexp2.MustCompile("", 0),
			errorAllowList: regexp2.MustCompile("", 0),
			errorDenyList:  regexp2.MustCompile("", 0),
		noDeadline       bool
			noDeadline:      false,
			noDeadline:       false,
		{
			scenario: "context has no deadline",
			request: func() *http.Request {
				req := buildMultipartFormDataRequest()
				req.Header.Set("Gotenberg-Webhook-Url", "foo")
				req.Header.Set("Gotenberg-Webhook-Error-Url", "bar")
				return req
			}(),
			mod:         buildWebhookModule(),
			noDeadline:  true,
			expectError: true,
		},
				mod.allowList = regexp2.MustCompile("bar", 0)
			noDeadline:       false,
				mod.denyList = regexp2.MustCompile("foo", 0)
			noDeadline:       false,
				mod.errorAllowList = regexp2.MustCompile("foo", 0)
			noDeadline:       false,
				mod.errorDenyList = regexp2.MustCompile("bar", 0)
			noDeadline:       false,
			noDeadline:       false,
			noDeadline:       false,
			noDeadline:       false,
			noDeadline:       false,
			noDeadline:       false,
			noDeadline:       false,
			if tc.noDeadline {
				ctx := &api.ContextMock{Context: &api.Context{Context: context.Background()}}
				ctx.SetEchoContext(c)
				c.Set("context", ctx.Context)
				c.Set("cancel", func() context.CancelFunc {
					return nil
				}())
			} else {
				timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
				ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}}
				ctx.SetEchoContext(c)
				c.Set("context", ctx.Context)
				c.Set("cancel", cancel)
			}
			allowList:      regexp2.MustCompile("", 0),
			denyList:       regexp2.MustCompile("", 0),
			errorAllowList: regexp2.MustCompile("", 0),
			errorDenyList:  regexp2.MustCompile("", 0),
			timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
			ctx := &api.ContextMock{Context: &api.Context{Context: timeoutCtx}}
			c.Set("cancel", cancel)
	"github.com/dlclark/regexp2"
	allowList      *regexp2.Regexp
	denyList       *regexp2.Regexp
	errorAllowList *regexp2.Regexp
	errorDenyList  *regexp2.Regexp