Skip to content

v0.13.2

Choose a tag to compare

@github-actions github-actions released this 22 Oct 21:51
· 266 commits to main since this release
5dbb085

Security

  • The lazy JobStore.get() id lookup now validates the job id with a positive, anchored allowlist before building a filesystem path, clearing two HIGH CodeQL py/path-injection alerts. Job ids are server-generated (uuid.uuid4().hex), so the lookup already rejected path-separator/dot/.. input, but CodeQL's taint tracker does not recognise a negative traversal check as sanitization. Both path-construction sites now route through a single JobStore._record_path() barrier that admits an id only via _SAFE_JOB_ID = re.compile(r"\A[A-Za-z0-9_-]{1,128}\Z").fullmatch(...) — a strict superset of every real id, with no separator/dot/traversal possible — before the id is joined onto the jobs directory; the path.parent == root check stays as defense in depth. Behaviour is unchanged: every valid id (server uuids, resumed/rollback ids, on-disk record stems) still resolves and only malformed/traversal input is refused.

Fixed

  • Lidarr now registers its operator-configured URL with the provider boundary, so a private LAN LIDARR_URL is trusted for Lidarr's own outbound calls (like Plex already is). The folder MBID-name resolver's Lidarr call was rejected by the outbound URL policy (ProviderOutcome.REJECTED, "blocked by outbound URL policy") whenever LIDARR_URL resolved to a private/RFC-1918 address (e.g. 192.168.0.250:8686) and the configured URL was not otherwise resolvable at call time: the boundary grants a provider implicit trust of its own configured URL, but Lidarr (unlike Plex) never registered an explicit resolver, so trust relied on a fragile fallback. routes_lidarr (a startup route module) now calls provider_boundary.register_configured_url("lidarr", ...) at import time, reading the same live backend.app_runtime.LIDARR_URL the resolver uses (env, then the LIDARR_URL_FILE fallback and default). Trust stays scoped to exactly that one configured scheme/host/port for Lidarr's own calls; the SSRF guard is unchanged for every other address and provider.
  • Durable-job cold start is now fast and bounded regardless of accumulated job history. On a long-lived instance the jobs directory (WEB_MANAGER_DATA_DIR/jobs) had grown to ~2,796 records (~34 MB), and JobStore read and parsed every file synchronously at startup (_recover globbed and json.loads'd all of them before the import that constructs the store returned). Over ZFS/NFS this made cold start take ~3 minutes with the process stuck in disk-sleep, so the container stayed unhealthy the whole time — which looked like a hang and triggered an unnecessary deploy rollback. Startup now reads only the most recent JOB_STARTUP_LOAD_LIMIT records (default 200, newest by mtime) on the critical path; the long tail is handled off-path by a background thread that recovers any interrupted job and prunes terminal records older than JOB_RETENTION_DAYS (default 7) from disk. JobStore.get() lazily loads any record by id, so the Jobs API, rollback-by-job and resume still resolve any non-pruned job even when it is outside the startup window. recovery_required records and running jobs are never auto-pruned, and the lazy id lookup rejects path-traversal input. New env knobs JOB_STARTUP_LOAD_LIMIT and JOB_RETENTION_DAYS (see docs/CONFIGURATION.md / .env.example); 0 or less for the load limit restores the old read-everything behaviour.

Added

  • Regression guard: POST /api/library/folder-mbid-names/apply starts its heavy work in the job, not the request thread. A new route-level test (tests/test_folder_mbid_async_start.py) asserts the endpoint returns {ok, job_id} promptly (sub-second) while the album-folder walk under MUSIC_ROOT and the Lidarr resolver snapshot — the ~9,022-dir-over-NFS cost that could block a request for ~173s and time out a client — run inside the job body (on the job thread), never synchronously in the request thread. The confirmed-gate (non-dry-run without confirmed:true → 400) is checked without touching the walk/resolver. Test-only; the handler already delegated the walk/resolver to the job via jobs.start_python, and this locks that structure against regression.