v3.1.0 — Security & correctness hardening
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/setupnow requires a one-timesetup_tokenthat 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 corruptpassword.jsonis treated fail-closed (as "password set") instead of silently re-opening setup. - Logout DoS closed:
/api/logoutonly 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 forgeableX-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_pathvalidation now also requires the basename to bebird, preventing an authenticated user from pointing it at/bin/shor other host binaries.- Added security response headers (CSP,
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy) and disabledX-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_idfields (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
warningslist for silently-ignored invalid values; aMAX_ACCOUNTScap (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
fsyncthe 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/unhandledRejectionnow log andexit(1)so systemd restarts cleanly instead of the process limping on in an undefined state; the worker loop reschedules infinally, and stdout write failures can no longer stall it.- Added a
lastTickAtheartbeat and ahealthyflag on/api/statusto detect a silently dead worker.