feat(chromium): add --chromium-clear-storage to clear local storage between conversions (#919)

This commit is contained in:
Julien Neuhart
2026-08-13 14:56:46 +02:00
parent b213f2ffed
commit 7a730cfdc2
6 changed files with 110 additions and 0 deletions

View File

@@ -49,6 +49,7 @@ type browserArguments struct {
denyPublicIPs bool
clearCache bool
clearCookies bool
clearStorage bool
disableJavaScript bool
}
@@ -367,6 +368,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out
runtime.Enable(),
clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies),
clearStorageActionFunc(logger, b.arguments.clearStorage, url),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent),
@@ -393,6 +395,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u
runtime.Enable(),
clearCacheActionFunc(logger, b.arguments.clearCache),
clearCookiesActionFunc(logger, b.arguments.clearCookies),
clearStorageActionFunc(logger, b.arguments.clearStorage, url),
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent),

View File

@@ -477,6 +477,7 @@ func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
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-clear-cache", false, "Clear Chromium cache between each conversion")
fs.Bool("chromium-clear-cookies", false, "Clear Chromium cookies between each conversion")
fs.Bool("chromium-clear-storage", false, "Clear Chromium local storage between each conversion (session storage is already isolated per conversion)")
fs.Bool("chromium-disable-javascript", false, "Disable JavaScript")
fs.Bool("chromium-disable-routes", false, "Disable the routes")
@@ -528,6 +529,7 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
denyPublicIPs: flags.MustBool("chromium-deny-public-ips"),
clearCache: flags.MustBool("chromium-clear-cache"),
clearCookies: flags.MustBool("chromium-clear-cookies"),
clearStorage: flags.MustBool("chromium-clear-storage"),
disableJavaScript: flags.MustBool("chromium-disable-javascript"),
}

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
"net/url"
"os"
"strconv"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/storage"
"github.com/chromedp/chromedp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
@@ -345,6 +347,54 @@ func clearCookiesActionFunc(logger *slog.Logger, clear bool) chromedp.ActionFunc
}
}
// clearStorageActionFunc clears the converted origin's local storage before the
// page loads, so state written by a previous conversion of the same origin does
// not leak into this one. See https://github.com/gotenberg/gotenberg/issues/919.
//
// Session storage is not touched: each conversion runs in its own browsing
// context (a fresh tab), so it is already isolated and cannot leak. Local
// storage is per-origin and shared across tabs of the long-lived browser, so it
// is the only web storage that carries over.
func clearStorageActionFunc(logger *slog.Logger, clear bool, rawURL string) chromedp.ActionFunc {
return func(ctx context.Context) error {
if !clear {
logger.DebugContext(ctx, "local storage not cleared")
return nil
}
origin, ok := httpOrigin(rawURL)
if !ok {
// A file:// upload gets an opaque, per-request origin that is not
// shared between conversions, so there is nothing to clear.
logger.DebugContext(ctx, "local storage not cleared: non-http(s) origin is already isolated")
return nil
}
logger.DebugContext(ctx, fmt.Sprintf("clear local storage for %s", origin))
err := storage.ClearDataForOrigin(origin, string(storage.TypeLocalStorage)).Do(ctx)
if err == nil {
return nil
}
return fmt.Errorf("clear local storage: %w", err)
}
}
// httpOrigin returns the http(s) security origin (scheme://host[:port]) of
// rawURL, and false when rawURL is not http(s). A non-http(s) URL such as a
// file:// upload has an opaque origin that no other conversion shares.
func httpOrigin(rawURL string) (string, bool) {
parsed, err := url.Parse(rawURL)
if err != nil {
return "", false
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", false
}
return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host), true
}
func disableJavaScriptActionFunc(logger *slog.Logger, disable bool) chromedp.ActionFunc {
return func(ctx context.Context) error {
// See https://github.com/gotenberg/gotenberg/issues/175.