Skip to content

v3.1.0 — Security & correctness hardening

Choose a tag to compare

@xxvcc xxvcc released this 09 Jul 08:20

Security and correctness hardening release following a full three-round audit. Backward-compatible for existing installs (which already have a password); only fresh setups and password resets now require a one-time setup token.

Security

  • First-run takeover (TOFU) closed: /api/setup now requires a one-time setup_token that is printed to the server log at startup while no password exists, preventing an unauthenticated attacker from claiming the panel during the pre-setup window. A corrupt password.json is treated fail-closed (as "password set") instead of silently re-opening setup.
  • Logout DoS closed: /api/logout only bumps the session epoch (global revocation) when the request carries a valid session + CSRF token; unauthenticated requests can no longer force a global logout.
  • Login rate-limit bypass fixed: the failure counter is incremented synchronously before password verification, closing the concurrency window that let parallel requests slip past the lockout gate. The limiter is keyed on req.ip (not the forgeable X-Real-IP), clears when the lockout expires, and is bounded (lazy eviction + hard cap).
  • Password hashing/verification moved to async bcrypt so bursts of login attempts no longer block the event loop.
  • bird_path validation now also requires the basename to be bird, preventing an authenticated user from pointing it at /bin/sh or other host binaries.
  • Added security response headers (CSP, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy) and disabled X-Powered-By.
  • The SSE stream re-verifies the session on each keepalive (closing on logout/expiry) and enforces a connection cap.
  • Malformed cookies and session tokens now return 400/401 instead of leaking a 500 stack trace; a global error handler returns generic messages.

Fixed

  • Silent mis-push on empty first fetch: a new account whose first fetch returns an empty list no longer records an empty baseline, which previously made the next fetch push the entire timeline.
  • Fetch errors no longer masquerade as success: a non-zero exit / timeout from bird is treated as a failure instead of "no new tweets".
  • JSON is extracted from stdout only, so bracket characters in bird's stderr diagnostics can no longer corrupt parsing and drop a successful fetch.
  • Tweet IDs prefer the string id_str/rest_id fields (avoiding 64-bit float rounding collisions that dropped tweets); object-typed IDs are skipped.
  • Telegram 429 responses honor retry_after; permanent (4xx) errors are no longer retried.
  • Duplicate IDs within a single fetch (e.g. a pinned tweet appearing twice) are de-duplicated so they are not pushed twice.
  • Offset-less tweet timestamps are interpreted as UTC instead of the server's local timezone.
  • Usernames are de-duplicated case-insensitively; reserved names such as __proto__ no longer corrupt the dedup table (null-prototype maps).
  • The "checking…" badge no longer sticks on error paths (reset in finally).

Changed

  • Password reset now requires stop → delete password.json → restart, then reading the new one-time setup token from the logs.
  • Account checks run with bounded concurrency (max 4) so one slow/dead account no longer stalls the others; pause now takes effect immediately, mid-tick.
  • Config saves return a warnings list for silently-ignored invalid values; a MAX_ACCOUNTS cap (100) is enforced and duplicate accounts are merged.
  • Per-account dedup is flushed once per check instead of per tweet, reducing write amplification.

Robustness

  • Atomic writes now fsync the file and the directory, so the "safe against power loss" guarantee actually holds; corrupt data files are surfaced (logged and preserved) rather than silently reset to defaults.
  • uncaughtException / unhandledRejection now log and exit(1) so systemd restarts cleanly instead of the process limping on in an undefined state; the worker loop reschedules in finally, and stdout write failures can no longer stall it.
  • Added a lastTickAt heartbeat and a healthy flag on /api/status to detect a silently dead worker.