Skip to content

Releases: openVESSL/Anchorr

v1.4.9

03 Apr 09:51
e94e2b9

Choose a tag to compare

⚠️ 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/config

On Unraid: set the host path permissions to 777 in the share settings.


🔒 Security

  • Content-Security-Policy header: The dashboard now sends a Content-Security-Policy header restricting scripts to 'self' and cdn.jsdelivr.net. Inline scripts have been moved to external files so no unsafe-inline is needed for scripts
  • Container no longer runs as root: USER app is 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_token cookie's Secure flag previously trusted the X-Forwarded-Proto header directly, allowing any client to fake HTTPS over plain HTTP. It now relies solely on req.secure, which Express sets correctly when TRUST_PROXY is configured
  • Dependencies updated: npm audit fix resolved 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

02 Apr 12:28
f13e8f5

Choose a tag to compare

✨ 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/UNDEFINED only
  • Webhook debug log sanitized: No longer dumps the full payload — only logs ItemType, ItemId, and Name
  • XSS fix — log viewer: timestamp and level fields 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 style attribute

🐛 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

27 Mar 22:11

Choose a tag to compare

🐛 Bug Fixes

  • Library channel mapping broken: The config UI was saving undefined as the library key in JELLYFIN_NOTIFICATION_LIBRARIES — Jellyfin's /Library/VirtualFolders returns ItemId, not Id. All per-library channel assignments were silently ignored
  • Webhook channel routing mismatch: Jellyfin webhook payloads send CollectionId as LibraryId, while the config stores VirtualFolder ItemId — added a resolution step so the IDs are correctly matched
  • Poller library fallback broken: findLibraryId was called with a Set instead of the required Map<id, libraryObject>, causing traversal fallback to always return undefined
  • 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 using TIMEOUTS.TMDB_API instead of TIMEOUTS.SEERR_API
  • JELLYFIN_BASE_URL typo: GET /jellyfin/libraries was reading process.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 to createRequire so the version loads correctly

🏗️ Code Quality

  • Removed dead fetchRootFolders function and related cache variables (leftover from a discarded feature)
  • EMBED_COLOR_SEASON added to config template so it is configurable via the dashboard

v1.4.6

25 Mar 11:25

Choose a tag to compare

🔒 Security

  • Web dashboard binds to localhost by default: Bare-metal installs now bind to 127.0.0.1 instead of all interfaces. Set BIND_HOST=0.0.0.0 if you need external access (Docker Compose does this automatically)
  • Rate limiting: /start-bot, /stop-bot, and /auth/check are now rate-limited
  • SSRF hardening: All URLs passed to axios are now constructed via URL object 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

21 Mar 10:23
81500b3

Choose a tag to compare

What's Changed

🐛 Fixed

  • Trust proxy validation error: TRUST_PROXY=true now sets trust proxy to 1 (hop count) instead of true (boolean), resolving ERR_ERL_PERMISSIVE_TRUST_PROXY from express-rate-limit v7+ when running behind a reverse proxy like Traefik
  • Docker healthcheck: Default docker-compose.yml healthcheck was hitting /api/config (auth-gated, always 401) instead of /api/health (public). Fixed to use the correct endpoint

🏗️ Code Quality

  • SRP refactor: app.js split into focused modules under routes/, bot/, jellyfin/, and utils/ — 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

16 Mar 16:17
7c704b7

Choose a tag to compare

🔒 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: discordUserId is now escaped with escapeHtml() before being placed in the onclick attribute, 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 sends Content-Type: text/plain. The endpoint now uses express.json({ type: "*/*" }) to accept any content type
  • Webhook debounce error no longer blocks a series: A failed Discord send previously left a level: -1 temp marker in sentNotifications, 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 to SEERR_*. Existing config.json is migrated automatically on first boot. If you have JELLYSEERR_* set as environment variables outside of config.json, rename them manually
  • Pending DM requests survive restarts: pendingRequests is persisted to pending-requests.json (next to config.json, mode 0600) on every write and loaded on bot startup — users who requested media via /request now 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 ItemType and Name for 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 unbounded setTimeout handle accumulation under sustained login attacks
  • Remove redundant Map.get call in the login handler immediately after recordFailure
  • /api/webhook-secret returns the in-memory WEBHOOK_SECRET constant instead of calling readConfig() 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

15 Mar 11:08

Choose a tag to compare

🔒 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.com before being set as img.src
  • i18n sanitization: Translation strings are sanitized before innerHTML injection — strips <script>, event handlers, and javascript: 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 jti claim; 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

14 Mar 11:40
56ed59a

Choose a tag to compare

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_SECRET on first start. All incoming webhook requests must include a valid X-Webhook-Secret header or they will be rejected with a 401.
  • Timing-safe secret comparison — Replaced naive string comparison with crypto.timingSafeEqual to prevent timing attacks.
  • URL injection fixbuildJellyfinUrl now always uses the configured JELLYFIN_BASE_URL and ignores any ServerUrl provided 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

  1. Pull the new image and restart Anchorr
  2. Open the Anchorr dashboard and copy your WEBHOOK_SECRET
  3. In your Jellyfin webhook plugin, delete your existing Discord Destination and create a new Generic Destination
  4. Enter the Anchorr webhook URL in the Webhook URL field and configure your notification types as before
  5. Scroll down to the Headers section, click Add Header, set the name to X-Webhook-Secret, and paste your secret as the value
  6. Save

v1.4.0

11 Feb 14:36

Choose a tag to compare

[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 /request command 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.js with 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

26 Dec 20:22

Choose a tag to compare

[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