check / check (push) Successful in 2m30s
The encrypted /v1/e/ route built its image request straight from the decrypted payload, so a token could request an over-limit dimension (reaching libvips and exhausting memory) or an unknown fit mode (surfacing as a 500 from the processor). The token generator discarded every strconv.Atoi error, silently turning non-numeric width, height, quality, or ttl into 0 and applying no upper bound on dimensions. Add a shared ValidateImageRequest in internal/imgcache enforcing the MaxDimension bound and ValidateFitMode, and apply it on both the plain image route and the encrypted route so both reject an over-limit size or an unrecognized fit mode with 400. The generator now parses each numeric field explicitly and returns 400 naming the field for non-numeric or out-of-range input, with width and height bounds-checked so an unusable token cannot be minted. Model: opus-4-8
8.8 KiB
8.8 KiB
Workflow
- branch per issue from
next - do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (
TODO.mdchanges in the same commit as the work) - open a PR based on
next - an independent reviewer who did not write the change gates it
- the manager squash-merges the PR into
nextonce review passes nextstays green and mergeable tomainat any time; only the owner mergesnextintomain, via the single milestone PR- push
Status
pre-1.0. No git tags exist. The 1.0.0 milestone is in progress; work
lands on next, and main receives only the milestone PR that the
owner merges. next is at the canonical golangci-lint v2.12.2 config
and is green. Recent work extracted the internal/magic,
internal/allowlist, internal/httpfetcher, and internal/signature
packages. The gosec findings from the 2026-07-06 survey are resolved.
The disk cache is now size-bounded with LRU eviction
(cache_max_bytes), closing the unbounded disk growth DoS vector.
Next Step
P1: implement blocked networks configuration to extend SSRF protection
Completed Steps
- 2026-09-21 validate dimensions and fit mode on the encrypted-URL
route and the token generator (closes #62): added a shared
ValidateImageRequestininternal/imgcacheenforcing theMaxDimensionbound andValidateFitMode, applied by both the/v1/image/and/v1/e/routes, so an over-limit size or an unknown fit mode is a 400 rather than an out-of-memory or a 500 from the processor; the URL generator now checks every numeric form field and rejects a non-numeric or out-of-rangewidth,height,quality, orttlwith a 400 naming the field instead of coercing it to0, andwidth/heightare bounds-checked so an unusable token cannot be minted - 2026-09-21 http.Server hardening (closes #92): added
HTTPReadHeaderTimeout(10s, bounds the slowloris header dribble) andHTTPIdleTimeout(120s, bounds keep-alive reuse) alongside the existing timeouts and wired them onto the server; added aLimitBodymiddleware capping the two form POST bodies (POST /,POST /generate) atMaxFormBytes(1 MiB) and returning 413, applied ahead of the CSRF middleware so an oversized body is refused as 413 rather than being read as a missing CSRF token (403); leftWriteTimeoutat 60s unchanged - 2026-08-07 update golangci-lint to v2.12.2 with the canonical
.golangci.yml(v2 schema,default: allminus six disabled linters,lll88, tests included): bumped the pinnedgolangci/golangci-lint:v2.12.2-alpineimage inDockerfileand the release-archive sha256 pins inscript/bootstrap; fixed the findings the stricter config surfaced (notablyparalleltest,wsl_v5,goconst,lll,noinlineerr,err113,errcheck,testpackage— white-box test files renamed to*_internal_test.go), including #55's code absorbed after it merged, iterating the pinned linter to0 issues.; no single finding total is substantiable, since golangci-lint'suniq-by-linereveals new findings on a line as others there are fixed — the documented re-measurements were 81 after the #53 merge and 149 after the #55 merge; three behavior changes, so not a pure no-op:Cache.StoreVariantnow takes acontext.Context(noctx), so a cancelled request skips its best-effort accounting row;MetadataStorage.Store's cleanup defer was dead onmainand leaked.tmp-*.jsonon failure, now fixed with explicit removals; and thesigning_keyvalidation error text gainedvalue too short:; the eviction loop's uncancellable context is deferred to #102 under a//nolint:contextcheck; three//nolint:tagliatelledirectives keep the snake_case JSON wire/disk formats unchanged;make checkgreen - 2026-08-07 implement cache size management and eviction (closes
#51): new
cache_max_bytesconfig key validated by the startup framework (explicit values used exactly with no floor,0disables the disk cache entirely, omitted defaults to max(75% of free space on the filesystem containing<state_dir>/cache/, 500 MiB), logged at startup); processed variants are now tracked in the database (a newvariant_contenttable and an LRU timestamp onsource_content) so total usage is two SUMs, never a directory scan on the hot path; a background goroutine evicts globally least-recently-used entries (variants and source blobs merged) to the limit, woken by a periodic ticker and by write-pressure notifications from stores; a source blob and ALL of itssource_metadatareferences are deleted in one transaction before the file is unlinked, so multi-referenced blobs are never removed while referenced and rows never point at deleted files; a startup and periodic reconciliation pass adopts untracked variant files, drops rows for missing files, removes unreachable source blobs, and sweeps stale temp files - 2026-08-07 validate configuration on startup, fail fast on bad
config (closes #52): a config value that is set but unparseable or
invalid aborts startup naming the key and value (defaults apply only
to omitted keys), unknown config keys abort startup, a malformed
config file aborts instead of being skipped, and
state_diris verified creatable and writable before the listener binds - 2026-08-07 manual test pass of the auth and encrypted URL flows
against a locally built and running
pixad(built frommainat6573b9d, port 18099, local throwaway config); all six checks passed, plus all nine tests inscripts/manual-test.sh(closes #49):- visit
/and see the login form: HTTP 200,Pixa - Loginpage withname="key"password form - wrong key shows an error: POST
/withkey=wrong-keyreturned HTTP 200 login page containing "Invalid signing key" - correct signing key shows the generator form: POST
/returned HTTP 303 to/withSet-Cookie: pixa_session=...; HttpOnly; Secure; SameSite=Strict; GET/with that cookie renderedPixa - URL Generatorwith the/generateform and logout link - a generated encrypted URL serves the image: POST
/generate(ttl=3600) produced a/v1/e/<token>/img.jpegURL that returned HTTP 200,Content-Type: image/jpeg, an 800x600 baseline JPEG of 61706 bytes - an expired URL (short TTL) returns 410: a ttl=1 URL fetched
after 3 s returned HTTP 410 Gone with
{"error":"URL has expired","status":410,...} - logout redirects back to login: GET
/logoutreturned HTTP 303 to/withSet-Cookie: pixa_session=; Max-Age=0; subsequent GET/rendered the login form again
- visit
- 2026-08-07 fix the two remaining gosec findings (G124 in
internal/session): session cookies now always carry
Secure/HttpOnly/SameSite=Strict on both the set and clear paths;
make checkgreen (closes #47) - 2026-07-07 Adopted scripts-to-rule-them-all:
script/entrypoints, Makefile shims, README Entrypoints section - 2026-04-07 extract magic byte detection into internal/magic (#42)
- 2026-03-25 extract allowlist package from internal/imgcache (#41)
- 2026-03-25 move schema_migrations table creation into 000.sql (#36)
- 2026-03-20 enforce and document exact-match-only signature verification (#40)
- 2026-03-20 bound imageprocessor.Process input read to prevent unbounded memory use (#37); consolidate appname into an internal/globals constant (#34)
- 2026-03-18 parse version prefix from migration filenames (#33)
- 2026-03-15 QA audit fixes for 1.0/MVP readiness (#25)
- 2026-03-02 split Dockerfile with pre-built golangci-lint stage for faster CI (#23)
- 2026-02-25 repo policy compliance: CI workflow, hash-pinned images, golangci-lint and gosec fixes of that date (#14); arm64 Docker build fix (#16)
- 2026-01-08 WebP and AVIF encoding support via govips (both former P0 image processing items, now done)
Future Steps
- P1: rate limit global concurrent upstream fetches to prevent resource exhaustion
- P1: strip EXIF and other metadata from processed images (privacy)
- P2: security
- referer blacklist
- per-IP rate limiting
- per-origin rate limiting
- P2: HTTP response handling
- Last-Modified headers
- Vary header for content negotiation
- X-Request-ID propagation
- P2: auto format selection (format=auto based on Accept header)
- P2: configuration
- add all configuration options from README
- environment variable overrides
- YAML config file support
- P2: operational
- optional Sentry error reporting
- comprehensive request logging
- Prometheus performance metrics
- integration tests for the image proxy flow
- load tests to verify the 1k to 5k req/s target
- P2: documentation
- configuration options
- API endpoints
- deployment guide
- example nginx or caddy reverse proxy config