feat(anti-spam): rate limits + link filter + honeypot (LC-94) #154
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-94-anti-spam"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements LC-94 in full: rate limits, link filter (block / quarantine / warn), honeypot on register. Three logical commits inside the branch so the review can move per-subsystem.
Locked design choices before coding (per maintainer):
*.tk,*.ml,*.ga,*.cf) seeded withquarantineaction so admins see them and decide. Operators can add their own rules at/admin/link-filter.Foundation (
cbf18fc)chat/0032_anti_spam.sql:messages.quarantined,link_filter_rules,link_filter_quarantine, seeded TLDs.settings/0004_anti_spam.sql: three new toggle / cap keys.rate_limitmodule: fixed-window per (kind, key) on a DashMap. Zero limit = disabled.db::anti_spam: rules CRUD + quarantine queue (approve/reject wrap message + queue updates in a single tx).links:extract_hostswalks linkify hits and pullsurl::Url::host_str;pattern_matchesconverts the admin's literal-or-glob pattern to an anchored regex.AppStategets arate_limits: RateLimitsfield; main.rs + every test literal updated.Enforcement (
228be41)routes/room.rs::post_message: per-user rate cap before any DB work; link filter walks URLs and routes to block/quarantine/warn. Quarantine flips the row'squarantinedbit + inserts queue row + audit log + skips broadcast.routes/auth.rs::post_register: honeypot rejects bot fills with a generic 422 inline error (no signal to the spammer); per-IP rate limit ahead of validation.routes/password_reset.rs::post_forgot: per-IP rate limit ahead of email validation.db/chat.rs+db/{inbox,activity,uploads,bookmarks,pinned}.rs+digest.rs: every user-visible read path now joinsAND m.quarantined = 0so quarantined messages never leak into the room, unread counts, inbox, activity feed, file browser, bookmarks, pinned strip, or email digest.AppError::TooManyRequests-> 429.Admin UI (
b33e4ad)/admin/anti-spam: toggles + caps. Saved-state flash;anti_spam_settingsaudit row./admin/link-filter: add / list / delete rules with inline error on duplicates./admin/quarantine: pending queue with approve / reject. Approve unhides, reject soft-deletes; both audit-logged.admin_layout.html.Test plan
just check(both feature builds, clippy -D warnings, fmt --check).just test(50 binaries + newroutes_anti_spam12-case file). Pre-existing flakefile_serve_round_trips_uploaded_bytes(CLAUDE.md "out of scope") passes in isolation.just test-saasgreen; the 5 admin-page tests are#[cfg(feature = "standalone")].Limitations / follow-ups
server/src/views/room.rs:387) still reads soft-deleted via SELECT-and-filter-in-Rust; not extended toquarantined. Quarantined messages can be quoted briefly before approve/reject; broadcast still skipped so visibility is minimal. Worth a follow-up if it matters.- routes/room.rs::post_message: per-user fixed-window rate limit (cap from settings.rate_limit_messages, 0=disabled); link filter walks linkify hits, runs each host through link_filter_rules. Block rejects the send; quarantine inserts with messages.quarantined=1 + link_filter_quarantine row + audit entry, skips broadcast; warn passes through + audit entry. - routes/auth.rs::post_register: honeypot hidden field on the register form rejects bots that auto-fill every input; per-IP rate limit on /register; both gates skip cleanly when their respective settings are off. - routes/password_reset.rs::post_forgot: per-IP rate limit on /forgot. - db/chat.rs + db/{inbox,activity,uploads,bookmarks,pinned}.rs + digest.rs: every user-visible read path now joins `AND m.quarantined = 0` next to the existing `m.deleted_at IS NULL` so quarantined messages never leak into the room, unread counts, inbox, activity feed, file browser, bookmarks, pinned strip, or email digest. - settings/0004_anti_spam.sql seeds the three new toggle keys + IP-cap keys (caps default 0 = disabled; toggles default true so the link filter and honeypot are on out of the box). - error.rs: new AppError::TooManyRequests -> 429. - AppState + every test literal grow a `rate_limits: RateLimits::new()` field. - 0032 backfilled into the hand-rolled migration arrays / verbose blocks.