GET ?password= and X-Paste-Password header checks in handleGetPaste were
unlimited (pentest2: 8 wrong = 8x401, no 429). Gate every password
verification on the same per-IP+paste unlock limiter (5/min, 429 with
Retry-After) already used by the POST unlock form path.
Regression tests: repeated wrong passwords via GET ?password= and via
header must eventually 429; correct password within burst still 200.
Fixes#81
- DELETE now demands the create-time deletion token (Authorization
header: Bearer/Token/bare, or ?token= query param), compared with
the constant-time store.DeletionTokenEqual. 403 otherwise.
- Creator-browser deletes via the /mine button (matching vwr cookie,
#37) remain allowed; other browsers and plain API clients get 403.
- Regression tests: no token, wrong token (header+query), correct
token (header+query), creator-cookie path, token extraction.
- Adapted TestSoftDelete to pass the deletion token.
- docs/API.md delete section updated.
- Based on #58's SoftDelete (bool, error) signature.
- enforce server-side body cap via http.MaxBytesReader: oversized JSON
bodies are rejected with 413 instead of being fully decoded first
- negative burn_after_reads rejected with 400 (zero still = default 1)
- limit=0 explicitly maps to default page size; shared parseLimit clamp
for /api/public and /api/mine (huge/non-numeric values too)
- negative/non-numeric offset clamped to 0 via parseOffset (was
pass-through)
- regression tests in issue68_validation_test.go
expires_at/expires_in validation intentionally excluded: covered by #60.
SoftDelete now reports whether it performed the delete (conditional
UPDATE ... WHERE deleted_at IS NULL checked via RowsAffected).
RegisterRead returns an admitted flag: legacy burn pastes admit exactly
one reader (the atomic soft-delete winner), and burn-after-N pastes
increment reads_used via a conditional UPDATE guarded on
reads_used < reads_limit, so concurrent readers cannot both consume the
final read. API, HTML, and raw read paths return 404 when the reader
loses the burn claim; content is never served twice.
OpenStore pins the SQLite pool to one connection: concurrent writes on
separate pooled connections surfaced SQLITE_BUSY as spurious 500s
instead of serializing.
Adds concurrency regression tests: 24 parallel readers of a burn paste
(exactly one receives content, none of the others leak it) and 30
parallel readers vs a 3-read budget (exactly 3 admitted, then 404).
- web.SecurityHeaders middleware wired into the chi router
- Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' (page scripts are inline); frame-ancestors 'none'
- Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff
- Applied only to text/html responses; JSON API and /raw pass through unchanged
- Regression test internal/web/securityheaders_test.go
- Fix filter fetch to request limit=100 (API max) instead of 500, which
the API silently clamped, so filtered results actually cover the fetch window.
- Document client-side filtering behavior and limits in README Performance Notes.
ListPublic and its COUNT query now filter password_hash IS NULL, so
/api/public (and any page backed by it) no longer leaks metadata
(title, slug, existence) of password-protected pastes. Unlisted
pastes were already excluded. Adds regression test covering both.
- attachments: filesystem-on-PVC recommended, blob interface keeps MinIO
as a later drop-in; size limits via admin setting; sniffed-mime +
nosniff/sandbox serving rules
- storage backend: stay SQLite-only (WAL, modernc); no Postgres/Redis and
no backend abstraction until documented trigger conditions fire
- new web/static/table.js (PaletteTable): shared live search, client-side
sort with indicators, pagination, row rendering + click-through
- history.html and mine.html both consume it; data endpoint, columns,
empty-state text and row extras (delete buttons) are page-supplied
- verified in-browser: search, sort, delete on /mine; sort, pager on /history
Closes#57
- #50 gutter/code line misalignment: shared --code-lh/--code-fs tokens;
.code .gutter drops its own vertical padding (was 14px vs 0, 14px offset)
- #51 history Paste column shows only the paste name, or slug pill when untitled
- #52 sort indicator arrow sits LEFT of the column label for all sortable columns
- #53 copy buttons give in-place success feedback ('Success!' in --ok for 2s):
paste view copy button and new-page result copy button
- #54 collapsed stats summary bar: roomier padding (14px 18px) and item gap (16px);
wraps gracefully on mobile
- #55 paste view title bar: slug pill only shown for custom slugs, id not repeated
Closes#50, closes#51, closes#52, closes#53, closes#54, closes#55
- vwr cookie middleware: random browser id set on first visit (reused by #49)
- pastes table gains viewer_id column, set server-side at creation from the cookie
- GET /api/mine lists pastes for the requesting browser (title/lang/size/created)
- DELETE enforcement: 403 when client-sent vwr doesn't match the paste's viewer_id
- /mine page reuses history table styling, delete buttons, empty state
- nav: 'Saved' item between Public and Git; Git gets external-link arrow (#56)
- tests: create-with-cookie appears in /mine, other cookie doesn't, delete enforcement
Closes#37
- Three new light [data-preset] blocks alongside midnight (default) and smooth
- --ok/--warn/--err semantic tokens with 4.5:1 contrast per preset
- .toast success/error variants wired in new.html
- ?theme= query param preview hook in layout head (screenshots only)
- Preview screenshots in docs/palette-previews/
Closes#16