From 88e1b1cdfaa9e4d7b1d165d5d35898c19f2b2056 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Thu, 15 Oct 2026 06:26:23 +0200 Subject: [PATCH] Allowlist no host in the example config (closes #249) The example config allowlisted github.com and user-images.githubusercontent.com, where anyone can put an image, so a deployment set up as Getting Started says served every image there to anyone without a signature. The example now allowlists nothing and shows one commented entry under example.com. README.md says in Getting Started and in "Running under upaas" that every image on an allowlisted host is served without a signature, so an allowlisted host must be one the operator controls. A new test loads the example config and fails if it allowlists any host. Model: opus-5-5 --- README.md | 9 ++++++++- TODO.md | 8 ++++++++ configs/config.example.yml | 11 +++++------ internal/config/config_internal_test.go | 26 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a9d14e1..250463e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ make docker docker run -p 8080:8080 -e PIXA_SIGNING_KEY="$(openssl rand -base64 32)" pixa:latest ``` +The example config allowlists no host (`allowlist_hosts`), so pixa serves an +image only for a signed or encrypted URL. Every image on an allowlisted host is +served to anyone without a signature, at any size and in any format, so +allowlist only hosts you control, where nobody else can put images. + A container takes its settings from environment variables (see Configuration below for the list). Only `PIXA_SIGNING_KEY` is required; if it is unset the container exits at startup naming the variable. Everything else has a built-in @@ -115,7 +120,9 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app for pixa needs: - `PIXA_SIGNING_KEY` (required): secret for signed and encrypted URLs and login, 32+ characters, for example from `openssl rand -base64 32` - `PIXA_ALLOWLIST_HOSTS`: upstream hosts served without a signature, - comma-separated + comma-separated; default none. Every image on a listed host is served to + anyone without a signature, so list only hosts you control, where nobody + else can put images. - `PIXA_CACHE_MAX_BYTES`: disk cache limit in bytes; `0` disables it; default 75% of (free space + what the cache holds) - the rest are in the table under Configuration below diff --git a/TODO.md b/TODO.md index 190ecec..b85bf37 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,14 @@ P2: security: per-origin rate limiting # Completed Steps +- 2026-10-15 the example config allowlists no host (closes #249): it allowlisted + `github.com` and `user-images.githubusercontent.com`, where anyone can put an + image, so a deployment set up as Getting Started says served those images to + anyone without a signature, at any size and in any format. + `configs/config.example.yml` now leaves `allowlist_hosts` out, with one + commented entry under `example.com`. Getting Started and "Running under upaas" + in `README.md` say that every image on an allowlisted host is served without a + signature, so an allowlisted host must be one the operator controls. - 2026-10-13 `fit=outside` no longer makes an output above 8192 on a side (closes #257): it scales the image to cover the requested size, and the side scaled past that size was held to no maximum, so `1x8192?fit=outside` of a diff --git a/configs/config.example.yml b/configs/config.example.yml index 2c59549..cdf1022 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -45,15 +45,14 @@ state_dir: ./data signing_key: "CHANGE_ME_generate_with_openssl_rand_base64_32" # Hosts that don't require signatures (default: none) +# Every image on an allowlisted host is served to anyone without a +# signature, at any size and in any format, so list only hosts you +# control, where nobody else can put images. # Use "." prefix for wildcard subdomain matching (e.g., ".example.com" matches "cdn.example.com") # An entry that is neither a host name nor an IP address (IPv6 without # brackets), such as one with a port or a "*." wildcard, aborts startup. -allowlist_hosts: - - s3.sneak.cloud - - static.sneak.cloud - - sneak.berlin - - github.com - - user-images.githubusercontent.com +# allowlist_hosts: +# - images.example.com # Hosts whose pages may not show pixa's images, written as for # allowlist_hosts. A request to /v1/image/ or /v1/e/ whose Referer header diff --git a/internal/config/config_internal_test.go b/internal/config/config_internal_test.go index de33d40..b655b35 100644 --- a/internal/config/config_internal_test.go +++ b/internal/config/config_internal_test.go @@ -92,6 +92,32 @@ func TestGetStringSlice_Empty(t *testing.T) { } } +// TestExampleConfigAllowlistsNoHost loads configs/config.example.yml +// with a real signing key, as Getting Started in README.md has it copied +// and edited, and checks that it allowlists no host: every image on an +// allowlisted host is served to anyone without a signature. +func TestExampleConfigAllowlistsNoHost(t *testing.T) { + t.Setenv("PIXA_SIGNING_KEY", validTestSigningKey) + + examplePath := filepath.Join("..", "..", "configs", "config.example.yml") + + sc, err := loadTestConfig(examplePath) + if err != nil { + t.Fatalf("failed to load %s: %v", examplePath, err) + } + + c, err := newFromSmartConfig(sc) + if err != nil { + t.Fatalf("%s is not a valid config: %v", examplePath, err) + } + + t.Logf("allowlist_hosts in %s: %v", examplePath, c.AllowlistHosts) + + if len(c.AllowlistHosts) != 0 { + t.Errorf("%s allowlists %v, want no host", examplePath, c.AllowlistHosts) + } +} + // loadTestConfig is a helper to load a config file for testing. func loadTestConfig(path string) (*smartconfig.Config, error) { return smartconfig.NewFromConfigPath(path)