Files
pixa/TODO.md
T
sneak 345a002cc2
check / check (push) Failing after 0s
feat: cap form POST body size and return 413 (closes #92)
Add a LimitBody middleware that caps the request body at MaxFormBytes
(1 MiB) on POST / and POST /generate and rejects an oversized body with
413. It parses the form under the cap before the CSRF middleware, which
reads its token from the body with PostFormValue and would otherwise see a
truncated body as a missing token (403); a successful parse is cached, so
the CSRF check and handler reuse it. Wired ahead of CSRF in SetupRoutes.

This makes the limit explicit rather than resting on ParseForm's incidental
10 MB cap, which would silently vanish if a handler switched to io.ReadAll
or multipart.

Model: opus-4-8
2026-09-21 18:22:22 +00:00

8.1 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.md changes 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 next once review passes
  • next stays green and mergeable to main at any time; only the owner merges next into main, 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 http.Server hardening (closes #92): added HTTPReadHeaderTimeout (10s, bounds the slowloris header dribble) and HTTPIdleTimeout (120s, bounds keep-alive reuse) alongside the existing timeouts and wired them onto the server; added a LimitBody middleware capping the two form POST bodies (POST /, POST /generate) at MaxFormBytes (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); left WriteTimeout at 60s unchanged
  • 2026-08-07 update golangci-lint to v2.12.2 with the canonical .golangci.yml (v2 schema, default: all minus six disabled linters, lll 88, tests included): bumped the pinned golangci/golangci-lint:v2.12.2-alpine image in Dockerfile and the release-archive sha256 pins in script/bootstrap; fixed the findings the stricter config surfaced (notably paralleltest, 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 to 0 issues.; no single finding total is substantiable, since golangci-lint's uniq-by-line reveals 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.StoreVariant now takes a context.Context (noctx), so a cancelled request skips its best-effort accounting row; MetadataStorage.Store's cleanup defer was dead on main and leaked .tmp-*.json on failure, now fixed with explicit removals; and the signing_key validation error text gained value too short: ; the eviction loop's uncancellable context is deferred to #102 under a //nolint:contextcheck; three //nolint:tagliatelle directives keep the snake_case JSON wire/disk formats unchanged; make check green
  • 2026-08-07 implement cache size management and eviction (closes #51): new cache_max_bytes config key validated by the startup framework (explicit values used exactly with no floor, 0 disables 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 new variant_content table and an LRU timestamp on source_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 its source_metadata references 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_dir is 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 from main at 6573b9d, port 18099, local throwaway config); all six checks passed, plus all nine tests in scripts/manual-test.sh (closes #49):
    • visit / and see the login form: HTTP 200, Pixa - Login page with name="key" password form
    • wrong key shows an error: POST / with key=wrong-key returned HTTP 200 login page containing "Invalid signing key"
    • correct signing key shows the generator form: POST / returned HTTP 303 to / with Set-Cookie: pixa_session=...; HttpOnly; Secure; SameSite=Strict; GET / with that cookie rendered Pixa - URL Generator with the /generate form and logout link
    • a generated encrypted URL serves the image: POST /generate (ttl=3600) produced a /v1/e/<token>/img.jpeg URL 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 /logout returned HTTP 303 to / with Set-Cookie: pixa_session=; Max-Age=0; subsequent GET / rendered the login form again
  • 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 check green (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