clawbot 275e145a6d
All checks were successful
check / check (push) Successful in 5s
fix: set Secure/HttpOnly/SameSite on session cookies (closes #47) (#48)
closes #47

Fixes the two remaining `gosec` findings on `main`, both `G124`
(http.Cookie missing or has insecure `Secure`, `HttpOnly`, or
`SameSite` attribute):

- `internal/session/session.go:84` (`CreateSession`, the login
  set-cookie path)
- `internal/session/session.go:128` (`ClearSession`, the logout
  delete-cookie path)

## What changed

- Both cookie-writing paths now unconditionally set `Secure: true`,
  `HttpOnly: true`, and `SameSite: http.SameSiteStrictMode`.
- The `secure` field (previously wired to `!config.Debug`) and the
  `sameSite` field are removed from `session.Manager`, and the dead
  secure-toggle parameter is removed from `session.NewManager`, which
  now takes only the signing key (reviewer-directed; the mechanical
  call-shape updates in `session_test.go` leave every assertion
  untouched).
- TDD per repo rules: the first commit adds
  `TestSessionCookieAttributesAlwaysSecure` (failing), asserting that
  every cookie emitted by the session manager carries `HttpOnly`,
  `Secure`, and `SameSite` of Lax or stricter, for both write paths.
  The second commit makes it pass.
- `TODO.md` updated per its Workflow section (Next Step completed,
  next Future Step promoted, stale "10 open findings" Status text
  corrected).

## Attribute choices and reasoning

- `Secure: true` always: the `G124` analyzer only accepts a constant
  `true` store, and there is no legitimate configuration in which the
  authentication cookie should be sent over plaintext HTTP. The old
  behavior disabled `Secure` whenever `debug` was on. Local development
  over `http://localhost` keeps working: browsers treat `localhost` as
  a trustworthy origin and accept `Secure` cookies there. Any
  plain-HTTP flow on a non-localhost host will no longer keep a
  session, which is the point of the fix.
- `SameSite: Strict` (unchanged from current production behavior, and
  stricter than the Lax minimum): the login form is a same-origin POST
  to `/` followed by a same-site redirect, so `Strict` breaks nothing.
- `HttpOnly: true` (unchanged).

## Verification

`make check` (tests, golangci-lint, fmt-check) is fully green on the
branch head `cb9e14e`: all tests pass and the linter reports 0 issues,
independently confirmed by the reviewer in a fresh worktree. Commit
history: `ca15f52` (failing test) → `02ca16a` (fix + TODO.md, closes
#47) → `cb9e14e` (drop the dead `NewManager` parameter).

Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #48
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-08-07 17:41:03 +02:00
2026-07-07 02:14:03 +02:00
2026-02-25 18:18:06 +07:00
2026-02-25 18:22:13 +07:00
2026-07-07 02:14:03 +02:00
2026-07-07 02:14:03 +02:00

pixa

pixa is a GPL-3.0-licensed Go web server by @sneak that proxies images from upstream sources, optionally resizing or transforming them, and serves the results. Both source and transformed images are cached to disk so that subsequent requests are served without origin fetches or additional processing.

Getting Started

# clone and build
git clone https://git.eeqj.de/sneak/pixa.git
cd pixa
make build

# run with a config file
./bin/pixad --config config.example.yml

# or build and run via Docker
make docker
docker run -p 8080:8080 pixad:latest

Rationale

Image-heavy web applications need a fast, caching reverse proxy that can resize and transcode images on the fly. pixa fills that role as a single, self-contained binary with no external runtime dependencies beyond libvips. It supports HMAC-SHA256 signed URLs with expiration to prevent abuse, and whitelisted source hosts for open access.

Design

Storage

  • Source content: <statedir>/cache/src-content/<ab>/<cd>/<sha256 of source content>
  • Source metadata: <statedir>/cache/src-metadata/<hostname>/<sha256 of path>.json (fetch time, original headers, request, content hash)
  • Database: <statedir>/state.sqlite3 (SQLite)
  • Output documents: <statedir>/cache/dst-content/<ab>/<cd>/<sha256 of output content>

Multiple source paths may reference the same content blob; the database tracks references rather than using filesystem refcounting. In-process caching of request-to-output mappings targets 1-5k r/s.

Routes

/v1/image/<host>/<path>/<size>.<format>?sig=<signature>&exp=<expiration>

Images are only fetched from origins using TLS with valid certificates.

  • <format>: one of orig, png, jpeg, webp
  • <size>: orig or <width>x<height> (e.g. 800x600)

Source Hosts

Source hosts may be whitelisted in the configuration. Non-whitelisted hosts require an HMAC-SHA256 signature.

Signature Specification

Signatures use HMAC-SHA256 and include an expiration timestamp to prevent replay attacks. Signatures are exact match only: every component (host, path, query, dimensions, format, expiration) must match exactly what was signed. No suffix matching, wildcard matching, or partial matching is supported.

Signed data format (colon-separated):

HMAC-SHA256(secret, "host:path:query:width:height:format:expiration")

Where:

  • host — source origin hostname (e.g. cdn.example.com)
  • path — source path (e.g. /photos/cat.jpg)
  • query — source query string, empty string if none
  • width — requested width in pixels, 0 for original
  • height — requested height in pixels, 0 for original
  • format — output format (jpeg, png, webp, avif, gif, orig)
  • expiration — Unix timestamp when signature expires

Example: resize https://cdn.example.com/photos/cat.jpg to 800x600 WebP with expiration 1704067200:

  1. Build input: cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200
  2. Compute HMAC-SHA256 with your secret key
  3. Base64URL-encode the result
  4. URL: /v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200

Whitelist patterns:

  • Exact match: cdn.example.com — matches only that host
  • Suffix match: .example.com — matches cdn.example.com, images.example.com, and example.com

Configuration

Configured via YAML file (--config). Key settings:

  • access_control_allow_origin — CORS origin
  • source_host_whitelist — list of allowed upstream hosts
  • upstream_fetch_timeout — timeout for origin requests
  • upstream_max_response_size — max origin response size
  • downstream_timeout — client response timeout
  • signing_key — HMAC secret for URL signatures

See config.example.yml for all options with defaults.

Architecture

  • Dependency injection: Uber fx
  • HTTP router: go-chi
  • Image processing: govips (CGO wrapper for libvips)
  • Database: SQLite via modernc.org/sqlite
  • Static assets: embedded via //go:embed
  • Metrics: Prometheus
  • Logging: stdlib slog

Entrypoints

This repository adheres to the Scripts to Rule Them All standard: normalized scripts in script/ are the entrypoints for the development workflow, and the Makefile targets are thin shims that call them. We provide:

  • script/bootstrap — install all dependencies (idempotent)
  • script/setup — make a fresh clone ready for development (bootstrap, then install-precommit)
  • script/projectname — output the project name ("pixa")
  • script/test — run the test suite
  • script/lint — run golangci-lint
  • script/fmt — format all code (writes)
  • script/fmt-check — check formatting (read-only)
  • script/check — run test, lint, and fmt-check
  • script/docker — build the Docker image tagged via script/projectname
  • script/cibuild — CI entrypoint: docker build . (the Dockerfile runs the checks, so a green build implies a green repo)
  • script/precommit — pre-commit checks (go mod tidy guard, then script/check)
  • script/install-precommit — install the git pre-commit hook that runs script/precommit

TODO

See TODO.md for the full prioritized task list.

License

GPL-3.0. See LICENSE.

Author

@sneak

Description
No description provided
Readme GPL-3.0 4 MiB
Languages
Go 91.9%
Shell 3.9%
HTML 3.1%
Makefile 0.6%
Dockerfile 0.5%