You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
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.