Releases: openVESSL/Anchorr
v1.4.9
⚠️ Migration Required
This release changes the Docker container to run as a non-root user. If you are upgrading from a previous version, your host-side config directory may be owned by root and the container will fail to write config.json.
Fix — run this for the directory you mapped to /usr/src/app/config:
chmod 777 /path/to/your/configOn Unraid: set the host path permissions to 777 in the share settings.
🔒 Security
- Content-Security-Policy header: The dashboard now sends a
Content-Security-Policyheader restricting scripts to'self'andcdn.jsdelivr.net. Inline scripts have been moved to external files so nounsafe-inlineis needed for scripts - Container no longer runs as root:
USER appis now active in the Dockerfile — the container runs as a non-root user at runtime - Cookie Secure flag no longer bypassable via spoofed header: The
auth_tokencookie'sSecureflag previously trusted theX-Forwarded-Protoheader directly, allowing any client to fake HTTPS over plain HTTP. It now relies solely onreq.secure, which Express sets correctly whenTRUST_PROXYis configured - Dependencies updated:
npm audit fixresolved all known CVEs in transitive dependencies
🐛 Bug Fixes
- Debounce seconds field showed wrong value after config load: The display input was not synced when config loaded from the server — now updated correctly
- Container permission failures now fail fast: If the config directory is not writable on startup, the container exits immediately with a clear error message instead of starting in a broken state
v1.4.8
✨ New Features
- Auto-Map from Seerr: Detects Seerr users who have linked their Discord account and lets you map them in one click. Shows a preview modal with checkboxes before saving — Discord usernames and avatars are resolved automatically via the bot
- Sync with Seerr: Companion button that checks existing bot mappings against Seerr and surfaces stale ones (Discord unlinked, ID changed, or Seerr user deleted) for bulk removal
🔒 Security
- Discord token no longer logged: Debug log previously emitted the first 6 characters of the bot token — now logs
SET/UNDEFINEDonly - Webhook debug log sanitized: No longer dumps the full payload — only logs
ItemType,ItemId, andName - XSS fix — log viewer:
timestampandlevelfields are now escaped before rendering into the dashboard - XSS fix — library selector: Jellyfin library name and ID are now escaped before inserting into the UI
- XSS fix — role color: Role color value is now validated against a strict hex pattern before use in a
styleattribute
🐛 Bug Fixes
- Bot no longer crashes after a few days: Unhandled promise rejections were calling
process.exit(1)— now logged as errors without terminating the process - Discord user dropdown missing members: Removed the 1000-member cap from
guild.members.fetch()and added a manual Discord User ID input field as fallback for offline/uncached members - Auto-Map breaking the dashboard: Discord lookups previously fired N parallel browser requests per modal open, exhausting the API rate limit and causing the dashboard to lose bot status and mappings — resolution is now done server-side (sequential, cache-first)
v1.4.7
🐛 Bug Fixes
- Library channel mapping broken: The config UI was saving
undefinedas the library key inJELLYFIN_NOTIFICATION_LIBRARIES— Jellyfin's/Library/VirtualFoldersreturnsItemId, notId. All per-library channel assignments were silently ignored - Webhook channel routing mismatch: Jellyfin webhook payloads send
CollectionIdasLibraryId, while the config storesVirtualFolder ItemId— added a resolution step so the IDs are correctly matched - Poller library fallback broken:
findLibraryIdwas called with aSetinstead of the requiredMap<id, libraryObject>, causing traversal fallback to always returnundefined - Seerr users not loading in User Mapping UI: Fixed for Seerr variants that return a plain array instead of
{ results: [...] } - Wrong timeout constant in
checkMediaStatus: Was usingTIMEOUTS.TMDB_APIinstead ofTIMEOUTS.SEERR_API JELLYFIN_BASE_URLtypo:GET /jellyfin/librarieswas readingprocess.env.JELLYFIN_URL(undefined), always returning 400- Dashboard version never displayed:
import ... with { type: "json" }requires Node 20.10+ but the Docker image runs Node 18 — switched tocreateRequireso the version loads correctly
🏗️ Code Quality
- Removed dead
fetchRootFoldersfunction and related cache variables (leftover from a discarded feature) EMBED_COLOR_SEASONadded to config template so it is configurable via the dashboard
v1.4.6
🔒 Security
- Web dashboard binds to localhost by default: Bare-metal installs now bind to
127.0.0.1instead of all interfaces. SetBIND_HOST=0.0.0.0if you need external access (Docker Compose does this automatically) - Rate limiting:
/start-bot,/stop-bot, and/auth/checkare now rate-limited - SSRF hardening: All URLs passed to axios are now constructed via
URLobject pathname manipulation instead of string interpolation - Translation sanitizer: Rewritten with a DOM-based allowlist parser, closing several XSS bypass vectors
- Misc: Partial API key no longer logged; request payload logging removed; TMDB IDs validated as integers; GitHub Actions workflow permissions scoped to least-privilege
🚀 Added
- Series poster and episode overview in single-episode notifications: Jellyfin webhook notifications for individual episodes now show the series poster and episode overview instead of generic placeholders
⚠️ Migration Notes
Docker Compose users: Add BIND_HOST=0.0.0.0 to your environment: section.
v1.4.5
What's Changed
🐛 Fixed
- Trust proxy validation error:
TRUST_PROXY=truenow setstrust proxyto1(hop count) instead oftrue(boolean), resolvingERR_ERL_PERMISSIVE_TRUST_PROXYfrom express-rate-limit v7+ when running behind a reverse proxy like Traefik - Docker healthcheck: Default
docker-compose.ymlhealthcheck was hitting/api/config(auth-gated, always 401) instead of/api/health(public). Fixed to use the correct endpoint
🏗️ Code Quality
- SRP refactor:
app.jssplit into focused modules underroutes/,bot/,jellyfin/, andutils/— no behavior changes, improves maintainability and testability
Full Changelog: https://github.com/openVESSL/Anchorr/blob/main/CHANGELOG.md
Documentation written with AI assistance; all code changes manually verified.
v1.4.3
🔒 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
v1.4.2 — Security patch: Stored XSS fixes
🔒 Security
This release patches two critical stored XSS vulnerabilities that allowed unprivileged users to exfiltrate all secrets stored in Anchorr (Discord token, API keys, JWT secret, password hashes) by injecting JavaScript into the admin dashboard.
All users are strongly encouraged to update immediately.
GHSA-qpmq-6wjc-w28q — Stored XSS via Discord member display names
Reported by @xdnewlun1
The Discord member dropdown rendered display names via innerHTML without sanitization. A guild member with a crafted display name could inject scripts that executed in the admin's browser session and exfiltrated credentials via GET /api/config.
GHSA-6mg4-788h-7g9g — Stored XSS via Jellyseerr usernames
Reported by @Rex50527
Jellyseerr usernames were injected into the dashboard via innerHTML without escaping. A Jellyseerr account with a crafted username could trigger the same credential exfiltration chain.
What was fixed
- DOM API rewrite: Discord member dropdown now uses
createElement/textContent— display names are treated as data, never markup - Avatar URL validation: Avatar URLs validated against
cdn.discordapp.combefore being set asimg.src - i18n sanitization: Translation strings are sanitized before
innerHTMLinjection — strips<script>, event handlers, andjavascript:URLs while preserving safe markup - Config sanitization: Sensitive fields are masked before being sent to the browser; the server detects masked placeholders on save to prevent credential loss
- JWT token revocation: Tokens now carry a
jticlaim; logout immediately invalidates the token server-side - Security response headers:
X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Referrer-Policy - Auth rate limiting: Login and register endpoints limited to 20 requests per 15 minutes per IP
🚀 Improvements
- Dynamic version display: Dashboard footer and About section now show the live version from the server
Credits
Vulnerabilities reported by @xdnewlun1 and @Rex50527.
v1.4.1 — Security Release
What's Changed
🔒 Security Fixes
This release addresses a critical security vulnerability that was responsibly reported by @whoopsi-daisy.
Anchorr's Discord webhook endpoint accepted arbitrary POST requests without verifying the sender or the structure of the request. The handler parsed the payload and forwarded several fields directly into the internal execution pipeline used by the job runner. Those fields were later interpolated into a command string that was executed through a shell context. Because the values from the webhook payload were not sanitized or escaped, it was possible to craft a payload that terminated the expected argument sequence and injected additional shell tokens. A specially crafted webhook request could therefore alter the command that the job runner executed, allowing arbitrary command execution under the privileges of the Anchorr service.
Fixes included in this release:
- Webhook authentication — Anchorr now auto-generates a
WEBHOOK_SECRETon first start. All incoming webhook requests must include a validX-Webhook-Secretheader or they will be rejected with a 401. - Timing-safe secret comparison — Replaced naive string comparison with
crypto.timingSafeEqualto prevent timing attacks. - URL injection fix —
buildJellyfinUrlnow always uses the configuredJELLYFIN_BASE_URLand ignores anyServerUrlprovided in the webhook payload, preventing URL injection via poisoned metadata. - Credential leak fix — Removed debug logs that exposed token prefixes to log files.
- Secrets encoded at rest — Sensitive fields in
config.json(tokens, API keys, secrets) are now base64-encoded. Existing configs are migrated automatically on next save. - Rate limiting — Added a rate limiter (60 req/min) to the webhook endpoint.
- Config validation — Config is now validated against a Joi schema on startup.
⚠️ Breaking Changes
1. Webhook secret required
The webhook endpoint now requires an X-Webhook-Secret header. Requests without a valid secret are rejected with a 401. The secret is auto-generated on first start and shown in the Anchorr dashboard with a copy button and setup instructions.
2. Switch to Generic Destination required
Jellyfin does not support custom HTTP headers for the Discord Destination type. Since Anchorr now requires an X-Webhook-Secret header on every request, the Discord Destination can no longer be used. You must delete your existing Discord Destination and recreate it as a Generic Destination — otherwise the header cannot be set and all webhook deliveries will be rejected with 401 Unauthorized.
Migration
- Pull the new image and restart Anchorr
- Open the Anchorr dashboard and copy your
WEBHOOK_SECRET - In your Jellyfin webhook plugin, delete your existing Discord Destination and create a new Generic Destination
- Enter the Anchorr webhook URL in the Webhook URL field and configure your notification types as before
- Scroll down to the Headers section, click Add Header, set the name to
X-Webhook-Secret, and paste your secret as the value - Save
v1.4.0
[1.4.0] - 2026-02-11
✨ Added
- Multi-Season Selection UI: Enhanced season selection for TV shows with more than 25 seasons by implementing multiple cascading select menus, overcoming Discord's 25-option limit per select menu. Users can now seamlessly select seasons from shows with extensive episode lists.
- Discord Threads Support: Added support for mapping Discord threads as notification channels. You can now send Jellyfin notifications to specific threads in addition to regular text channels, providing better organization for different content types or libraries
- Auto-Approve Requests: Implemented auto-approve functionality for media requests. When enabled, requests made through the bot are automatically approved in Jellyseerr without requiring manual approval
- Daily Pick Recommendations: New daily recommendation feature that sends a curated movie or TV show suggestion to your Discord channel. Users receive fresh content recommendations each day to discover new media to watch
- Quality Profile Selection: Added quality profile selection in the
/requestcommand with intelligent autocomplete support. Users can now specify their preferred quality profile (e.g., "1080p", "4K", "Anime") directly when requesting content - Server Selection for Requests: Implemented server selection functionality allowing users to choose specific Radarr or Sonarr servers when making media requests, providing better control over where content is downloaded
- Default Quality Profiles Configuration: New UI section in Jellyseerr settings for configuring default quality profiles and servers separately for movies (Radarr) and TV shows (Sonarr). These defaults are used when users don't specify a profile in their request
- Load Profiles & Servers Button: Added convenient "Load Profiles & Servers" button in Jellyseerr settings that fetches and populates all available quality profiles and servers from your configured Radarr/Sonarr instances
🔄 Changed
- Request Flow Enhancement: Improved the request workflow to support optional quality profile and server selection, making the bot more flexible for advanced users while remaining simple for basic use cases
- Autocomplete Intelligence: Enhanced autocomplete system to handle quality profiles alongside existing media search autocomplete
🏗️ Code Quality
- Jellyseerr API Module Expansion: Extended
api/jellyseerr.jswith new functions for fetching quality profiles and servers from Jellyseerr API - Request Parameter Handling: Improved request parameter validation and handling to support new optional fields (profileId, serverId) in media requests
v1.3.5
[1.3.5] - 2025-12-26
✨ Added
- Notification Testing: New testing section in Jellyfin Notifications settings with 6 test buttons (Movie, Series, Season, Episodes, and batch tests for seasons/episodes) to preview notification appearance with real data
- Embed Customization: Granular control over notification embed elements - individually toggle backdrop image, overview/description, genre, runtime, rating, and each button (Letterboxd, IMDb, Watch Now)
- Separate Channel Mapping: Added dedicated optional channel settings for episodes and seasons, allowing you to route different notification types to specific Discord channels
- Localizations: Added Swedish (sv) and German (de) language support with work-in-progress translations
- Quality Profile Integration: Radarr and Sonarr quality profile selection in Jellyseerr settings for movie and TV requests