Tiles could vanish without ever being deleted:
- Editing an item merged the editor's user_id into every save, so
updating a visible item (e.g. a shared user_id=0 tile) silently
reassigned ownership and hid it from everyone else. user_id is now
set on create only, and excluded from update input in both Item and
Tag controllers since it is mass-assignable.
- Deleting a user left their items orphaned with a dangling user_id,
invisible to all users forever. The user's items are now hard-deleted
with the account, and a data migration reassigns already-orphaned
items to the admin user so previously "lost" tiles reappear.
- The Item global scope's ownership filter had an ungrouped orWhere,
breaking operator precedence in any query that adds further clauses.
Includes regression coverage for ownership on create/update, user
deletion cleanup, and the orphan-recovery migration.
ItemController::appload() was declared ': ?string', so its two error
branches that 'return response()->json([...], 404)' had the JsonResponse
coerced through Response::__toString() into a raw HTTP message served as
an HTTP 200 body. Widen the return type to
'\Illuminate\Http\JsonResponse|string|null' so those branches emit
real 404 JSON responses. The method body is unchanged, so the happy path
still returns the same JSON string and the frontend contract is preserved.
Flip the endpoint characterization test to assert the corrected 404.
The custom TrustHosts middleware only overrode hosts(), so it inherited the
parent's shouldSpecifyTrustedHosts() gate, which skips enforcement whenever the
app runs in the local environment or under the test runner. Heimdall ships
APP_ENV=local by default (.env.example, copied to .env on install), so the
TRUSTED_HOSTS allow-list a user configures per the .env.example guidance was
never actually applied.
Override shouldSpecifyTrustedHosts() to tie enforcement to configuration
instead of environment: apply the allow-list whenever TRUSTED_HOSTS is set, in
any environment; when it is unset hosts() is empty and enforcement stays off,
preserving the historic no-restriction behaviour. Add handle()-driven tests
covering both the configured and unconfigured cases.
get_stats/{id} fataled when the item id was missing and 500'd whenever an
enhanced app's livestats() threw - a broken or updated remote app definition
(e.g. Komga) took the whole request down, and the frontend then stopped
refreshing that tile entirely.
getStats now returns valid JSON (200) with an inactive/empty payload when the
item is missing, has no class, references a stale class, or throws, logging
the failure for diagnosis. The successful path is unchanged and returns the
livestats output verbatim.
Resolves#1558
Heimdall trusted the incoming X-Forwarded-Host header for URL generation, so
a spoofed value poisoned the page base href, asset() URLs and redirect
targets - loading assets from and redirecting to an attacker-controlled host
(CVE-2025-50578).
- TrustProxies no longer trusts X-Forwarded-Host; a forged value can no longer
influence getHost(), url(), asset() or redirects. X-Forwarded-For/Port/Proto
handling is unchanged.
- Trusted proxies are now configurable via the TRUSTED_PROXIES env var
(comma-separated CIDRs/IPs, "*" to trust all), defaulting to the previous
private ranges.
- Added an opt-in TRUSTED_HOSTS allow-list: when set, only the listed hosts are
served and any other Host header is rejected. Unset keeps the historic
behaviour of serving arbitrary hosts, so existing installs are unaffected.
Resolves#1451
The export endpoint (api/item) now emits each item's assigned tag titles,
excluding the root/default dashboard tag. On import, those titles are
resolved back to local tags - reusing an existing tag or creating a missing
one - instead of dropping every imported item onto the default dashboard.
Tags round-trip by title so a config can be moved between instances without
having to reassign each item to its section by hand.
Resolves#1555