
Public advisory & PoC for CVE-2026-26897 — Deep Link Bypass in EcoOnline EHS Android (com.airsweb.v10), fixed in 0.2.500
Public disclosure / advisory for CVE-2026-26897, a deep link validation bypass in the
EcoOnline EHS Android application (com.airsweb.v10). A crafted ehs-app:// deep link is
rewritten to https:// and loaded directly into the app's main WebView without the host
allow-list check that the app applies on every other navigation path, letting a remote attacker
render arbitrary attacker-controlled content inside the trusted application (phishing).
Responsibly disclosed, acknowledged by the vendor, and fixed in version 0.2.500.
com.airsweb.v10 0.2.499 (versionCode 44631)The vendor's application is proprietary and is not redistributed in this repository. The static analysis below was performed on the build identified here — obtain the exact APK from an archive (e.g. APKPure version history) and verify its hash before reproducing:
| Package | com.airsweb.v10 |
| versionName / versionCode | 0.2.499 / 44631 |
EcoOnline EHS for Android is a Jetpack-Compose WebView wrapper around the EHS web platform. Its default content is loaded from:
startUrl = "https://ehsmobilelanding.ecoonline.net/"
The app exports a single Activity (MainActivity) that is both the launcher and the handler for the
custom URL scheme ehs-app. The <data> element of the deep-link intent-filter declares
only a scheme — no host and no path — so any ehs-app://… URI is routed to the app.
To protect WebView navigation, the app implements a host allow-list (isInternalHost) and applies it
when restoring the last-visited URL. However, the deep-link handling branch does not call that
allow-list at all. It takes the incoming deep-link URI, performs a naïve string replacement of the
scheme ehs-app: → https:, and calls WebView.loadUrl() on the result. A remote attacker can
therefore craft a deep link that causes the trusted app to load an arbitrary https:// origin.
AndroidManifest.xml (v0.2.499) — exported, scheme-only deep link<activity
android:name="com.airsweb.v10.MainActivity"
android:exported="true" ... >
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<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="ehs-app"/> <!-- scheme only: no host, no path -->
</intent-filter>
</activity>
MainActivityKt.WebViewPage (decompiled, v0.2.499)// intent delivered to the exported MainActivity
if (intent.getScheme().equals("ehs-app")) {
WebView webView = ...;
if (webView != null) {
Uri data = intent.getData(); // full ehs-app://… URI
String s = String.valueOf(data);
// naïve scheme rewrite: "ehs-app:" -> "https:"
String url = StringsKt.replaceFirst(s, "ehs-app:", "https:", false, 4, null);
webView.loadUrl(url); // <-- NO isInternalHost() CHECK
}
}
The allow-list that this path fails to apply (used elsewhere in the same file):
private static final Regex internalDomains =
new Regex("^[-\\w]*\\.(airsweb)|(ecoonline)\\.net");
private static final List<String> excludeDomainsStartingWith =
listOf("passport", "login", "auth");
public static boolean isInternalHost(String host) {
for (String p : excludeDomainsStartingWith)
if (host.startsWith(p)) return false;
return internalDomains.containsMatchIn(host);
}
Secondary note: the
internalDomainsregex is itself weak (broken alternation +containsMatchInsubstring matching), but the primary issue is that the deep-link branch never invokesisInternalHostat all.
A screen-recording PoC was produced on 2026-01-13 against com.airsweb.v10 0.2.499.
ehs-app://attacker.example/login
MainActivity rewrites the scheme and the WebView loads:
https://attacker.example/login
A benign, non-harvesting demonstration of the redirect is included in
poc/redirect-proof.html (usage: poc/README.md). It
proves an arbitrary origin is loaded inside the trusted app, with no inputs, no credential capture,
and no network requests.
In the original PoC the researcher hosted a benign look-alike landing page purely to demonstrate the redirect. This advisory documents the vulnerability for defensive / disclosure purposes and intentionally contains no phishing page or credential-capture code.
https:// content inside the trusted application context.CVSS v3.1 (researcher estimate — finalise before publication):
AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N → 6.3 (Medium) (a Scope:Changed argument can be made, as
untrusted content executes in the trusted app context).
Update to com.airsweb.v10 0.2.500 or later. Recommended fixes:
isInternalHost() (or an equivalent allow-list) to deep-link URLs before loadUrl().intent-filter with an explicit android:host/android:pathPrefix.internalDomains regex (anchor host fully; avoid substring matches).Discovered and reported by Ahmet Mersin — GitHub @iwallplace,
HackerOne iwallplace.
| SHA-256 |
e9e8a38d133cb9da9901caf03c4a7a033c75ad1f32ba0539260e47d5dd5068c0 |
| Source | APKPure — "EcoOnline EHS" version history |
| Field | Value |
|---|
| CVE ID | CVE-2026-26897 |
| Vendor | EcoOnline ("EcoOnline Global") — https://www.ecoonline.com/ |
| Product | EcoOnline EHS for Android (com.airsweb.v10) |
| Affected version | 0.2.499 (versionCode 44631) |
| Fixed version | 0.2.500 |
| Vulnerability type | Improper validation in a custom URL scheme handler (deep link bypass) → unvalidated WebView load / open redirect |
| CWE | CWE-939 (Improper Authorization in Handler for Custom URL Scheme); related CWE-749 |
| Affected component | com.airsweb.v10.MainActivity (exported deep-link handler) and the deep-link branch in MainActivityKt.WebViewPage |
| Custom scheme | ehs-app:// |
| Attack type | Remote (requires user interaction — opening a crafted link) |
| Impact | Phishing / social engineering: arbitrary attacker-controlled web content rendered inside the trusted EcoOnline EHS app (e.g. a fake login), bypassing the app's domain allow-list |
| Discoverer | Ahmet Mersin |
| Date | Event |
|---|
| 2026-01-13 | PoC recorded against 0.2.499 |
| 2026-01-15 | CVE request submitted to MITRE (ticket scr1979357) |
| 2026-02-27 | CVE-2026-26897 assigned by MITRE |
| prior to publication | Vendor acknowledged; fix released in 0.2.500 |
| 2026-03-04 | Publication request sent to MITRE |
| 2026-05-06 | MITRE requested a complete public disclosure URL |
| 2026-06-03 | Public advisory published (this repository) |