fix: validate Content-Length + add paste expiry/TTL #3

Merged
everestmcarthur merged 1 commit from fix/content-length-and-paste-ttl into main 2026-05-19 10:44:32 -04:00
everestmcarthur commented 2026-05-19 07:17:21 -04:00 (Migrated from github.com)

Summary

Fixes both open issues (#1 and #2) in a single PR.

🔒 Issue #2 — Content-Length validation (security)

Problem: do_POST reads Content-Length as int() without validation. A negative value (Content-Length: -1) causes rfile.read(-1) to read until EOF, hanging the server thread indefinitely.

Fix:

  • Reject negative Content-Length values with 400 Bad Request
  • Handle non-integer values gracefully (try/except ValueError)

⏰ Issue #1 — Paste expiry / TTL

Problem: Pastes are stored forever with no cleanup mechanism. Database grows indefinitely.

Fix:

  • New expires_at column on the pastes table
  • Default TTL: 30 days (configurable via --max-age DAYS CLI flag or FORGE_PASTE_MAX_AGE env var)
  • --max-age 0 disables expiry entirely
  • Opportunistic cleanup runs on each paste creation (throttled to max once/hour)
  • Expired pastes are excluded from reads immediately (no stale content served)
  • Viewer UI shows expiry countdown ("expires in 29d", "expires soon", etc.)
  • Auto-migration: existing databases get the new column added automatically, and existing pastes get backfilled with an expiry based on their creation date

Configuration

Method Example Notes
CLI flag --max-age 7 7-day TTL
Environment FORGE_PASTE_MAX_AGE=90 90-day TTL
Disable --max-age 0 Pastes never expire

Changes

  • forge-paste.py — +68/-8 lines
    • do_POST: Content-Length validation
    • init_db: new expires_at column + migration
    • create_paste: sets expiry, triggers cleanup
    • get_paste: filters out expired pastes
    • cleanup_expired(): new function, deletes expired rows + WAL checkpoint
    • page_viewer: shows expiry info
    • main(): new --max-age argument

Closes #1
Closes #2

## Summary Fixes both open issues (#1 and #2) in a single PR. ### 🔒 Issue #2 — Content-Length validation (security) **Problem:** `do_POST` reads `Content-Length` as `int()` without validation. A negative value (`Content-Length: -1`) causes `rfile.read(-1)` to read until EOF, hanging the server thread indefinitely. **Fix:** - Reject negative `Content-Length` values with `400 Bad Request` - Handle non-integer values gracefully (try/except `ValueError`) ### ⏰ Issue #1 — Paste expiry / TTL **Problem:** Pastes are stored forever with no cleanup mechanism. Database grows indefinitely. **Fix:** - New `expires_at` column on the `pastes` table - Default TTL: **30 days** (configurable via `--max-age DAYS` CLI flag or `FORGE_PASTE_MAX_AGE` env var) - `--max-age 0` disables expiry entirely - Opportunistic cleanup runs on each paste creation (throttled to max once/hour) - Expired pastes are excluded from reads immediately (no stale content served) - Viewer UI shows expiry countdown ("expires in 29d", "expires soon", etc.) - **Auto-migration:** existing databases get the new column added automatically, and existing pastes get backfilled with an expiry based on their creation date ### Configuration | Method | Example | Notes | | --- | --- | --- | | CLI flag | `--max-age 7` | 7-day TTL | | Environment | `FORGE_PASTE_MAX_AGE=90` | 90-day TTL | | Disable | `--max-age 0` | Pastes never expire | ### Changes - `forge-paste.py` — +68/-8 lines - `do_POST`: Content-Length validation - `init_db`: new `expires_at` column + migration - `create_paste`: sets expiry, triggers cleanup - `get_paste`: filters out expired pastes - `cleanup_expired()`: new function, deletes expired rows + WAL checkpoint - `page_viewer`: shows expiry info - `main()`: new `--max-age` argument Closes #1 Closes #2
Sign in to join this conversation.
No description provided.