🔒 Security
- Login brute-force protection (ref #80): Account locked for 10 minutes after 5 consecutive failed login attempts per username (HTTP 429 with seconds remaining). Progressive 300ms-per-attempt response delay (capped at 4 s) slows automated tools. bcrypt always runs even for unknown usernames to prevent user enumeration via timing. Existing IP-based rate limit (20 req / 15 min) remains as a first layer
- XSS fix — user mapping remove button:
discordUserIdis now escaped withescapeHtml()before being placed in theonclickattribute, and validated against Discord snowflake format (17–19 digits). Fixes stored XSS where a crafted Discord user ID could inject arbitrary JS into any admin's browser on page load
🐛 Fixed
- Jellyfin webhook Content-Type:
express.json()was silently dropping Jellyfin webhook bodies because Jellyfin sendsContent-Type: text/plain. The endpoint now usesexpress.json({ type: "*/*" })to accept any content type - Webhook debounce error no longer blocks a series: A failed Discord send previously left a
level: -1temp marker insentNotifications, blocking all future webhooks for that series for up to 24 hours. The marker is now deleted immediately on error, and the orphaned-marker cleanup timeout is reduced from 24 h to 5 min - Empty channel no longer crashes the webhook handler: When no Discord channel is configured for a library, the handler now logs a clear config error and returns cleanly instead of throwing a cryptic Discord API exception
🚀 Improvements
- Seerr rebrand: All
JELLYSEERR_*config keys renamed toSEERR_*. Existingconfig.jsonis migrated automatically on first boot. If you haveJELLYSEERR_*set as environment variables outside ofconfig.json, rename them manually - Pending DM requests survive restarts:
pendingRequestsis persisted topending-requests.json(next toconfig.json, mode 0600) on every write and loaded on bot startup — users who requested media via/requestnow receive their DM notification even if the bot was restarted before the media became available - Webhook secret visible on page load: The webhook secret field in the dashboard is now populated automatically on load so the value is immediately visible and copyable without digging through the config
- Better webhook error logs: Errors and warnings in the webhook handler now include
ItemTypeandNamefor easier debugging without parsing the raw payload
🏗️ Code Quality
- Fix timer leak in
auth.js: previous cleanup timer is cancelled before a new one is scheduled, preventing unboundedsetTimeouthandle accumulation under sustained login attacks - Remove redundant
Map.getcall in the login handler immediately afterrecordFailure /api/webhook-secretreturns the in-memoryWEBHOOK_SECRETconstant instead of callingreadConfig()on every request- Copy-secret button reads from the already-populated input field instead of making a second fetch to
/api/webhook-secret