OWASP Mobile Application Security Testing Guide: MASTG DEMO 0096

---
platform: ios
title: HTML Injection in a Local WebView Leading to Local File Access
code: [swift]
id: MASTG-DEMO-0096
test: MASTG-TEST-0333
kind: fail
---

## Sample

This sample demonstrates how overly broad file read access in a WebView can increase the impact of a separate HTML injection flaw. The app loads a trusted local HTML file and grants the WebView read access to the entire `Documents` directory by calling [`loadFileURL(_:allowingReadAccessTo:)`](https://developer.apple.com/documentation/webkit/wkwebview/loadfileurl(_:allowingreadaccessto:)).

By itself, that broad read access is not enough to expose files. The issue becomes exploitable because the page also reads the `username` parameter from the URL and inserts it into the DOM using `innerHTML`. Since attacker-controlled input is treated as HTML, an attacker can inject markup that loads other local files from the same directory. See @MASTG-DEMO-0095 for more details on the HTML injection aspect of the vulnerability.

Because the WebView can read the full `Documents` directory, injected elements such as an `<iframe>` can load sibling files like `secret.txt`. This shows how overly broad local file access can turn a separate WebView injection bug into a local file disclosure issue.

To exploit the demo and load the secret file stored at `<container>/Documents/secret.txt`, you can enter the following payload as input:

- `<iframe src="./secret.txt"></iframe>`
- `<object data="./secret.txt" type="text/plain"></object>`
- `<embed src="./secret.txt" type="text/plain">`

Summary of steps leading to this vulnerability.

1. The app loads a local HTML file into a `WKWebView`.
2. The WebView is granted read access to the entire `Documents` directory.
3. The page reads attacker controlled input from the `username` query parameter.
4. That value is inserted into the DOM using `innerHTML`.
5. The attacker injects markup that loads another local file, such as `secret.txt`.
6. As a result, local content becomes readable inside the WebView.

The vulnerable code path is the combination of untrusted input being inserted with `innerHTML` and broader local file read access than necessary. Together, they allow attacker controlled markup to access local files that should not be exposed.

{{ ../MASTG-DEMO-0095/MastgTest.swift # ../MASTG-DEMO-0095/ai-decompiled.swift }}

## Steps

1. Unzip the app package and locate the main binary file as described in @MASTG-TECH-0058. In this case, the binary is `./Payload/MASTestApp.app/MASTestApp`.
2. Open the app binary with @MASTG-TOOL-0073 and inspect the code paths that create and load the `WKWebView`.
3. Identify any calls that load content into the WebView, such as `load(_:)` or `loadFileURL(_:allowingReadAccessTo:)`.
4. Obtain the code that updates the page content after loading, to verify whether attacker-controlled input is inserted into the DOM using `innerHTML`.

{{ load_webview.r2 # run.sh }}

## Observation

The output shows the `loadFileURL:allowingReadAccessToURL:` call site, the `docDir` and `fileURL` lazy initializers, and the `innerHTML` assignment in the embedded HTML template.

{{ ../MASTG-DEMO-0095/showWebView.asm # output.txt }}

{{ docDir-init.asm # fileURL-init.asm }}

## Evaluation

The test fails because the app grants the WebView read access to the entire `Documents` directory using `loadFileURL(_:allowingReadAccessTo:)`. `docDir` points to the app's `Documents` directory, and it's passed directly as `allowingReadAccessTo` in `webView.loadFileURL(url, allowingReadAccessTo: docDir)`. This grants the WebView read access to the entire `Documents` directory, which also contains `secret.txt` (written in `createSecretFile`). An attacker can inject `<iframe src='./secret.txt'></iframe>` as their name to expose this file.

The attack succeeds because of the combination of this overly broad read access and the fact that attacker controlled input is inserted into the page. The decompiled code and local HTML show that attacker-controlled input (the `username` value) is inserted into the page by assigning it directly to `innerHTML` via JavaScript. Because this value is not HTML-escaped, tags such as `<iframe>`, `<img>`, and `<script>` are interpreted as markup, allowing the attacker's payload to be rendered as HTML. See @MASTG-DEMO-0095 for more details on the HTML injection aspect of the vulnerability.

The following analysis complements the one in @MASTG-DEMO-0095 by focusing on the overly broad file access aspect of the vulnerability.

### AI-Decompiled Code Analysis

!!! note "About `ai-decompiled.swift`"
    The `ai-decompiled.swift` file is an AI-assisted reconstruction derived from `showWebView.asm`, `docDir-init.asm`, and `fileURL-init.asm` and is provided only as a convenience for understanding the logic. It may be inaccurate or incomplete; the assembly and the original binary are the authoritative sources for analysis.

1. On **lines 10–12**, `docDir` is initialized to `FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!`, which resolves to the app's `Documents` directory — the same directory that contains `secret.txt`.
2. On **lines 14–16**, `fileURL` is built by appending `"index.html"` directly to `docDir`, so the loaded HTML file and any sensitive sibling files share the same parent directory.
3. On **line 35**, `webView.loadFileURL(url, allowingReadAccessTo: docDir)` passes `docDir` as the read-access root. Because `docDir` is the entire `Documents` directory, the WebView can reach any file inside it, including `secret.txt`.

### Disassembly Analysis

We can see the call to `loadFileURL:allowingReadAccessToURL:` at `0x100004f20` (in `showWebView.asm`). To determine which path is being granted read access, we need to trace both arguments.

Just before the call, the arguments are arranged as follows (from `showWebView.asm`):

```text
0x100004f10      ldr x1, [x8, 0x118]   ; reloc.fixup.loadFileURL:allowingReadAccessToURL:
0x100004f14      mov x0, x25           ; WKWebView instance
0x100004f18      mov x2, x26           ; urlWithUsername (NSURL)
0x100004f1c      mov x3, x20           ; allowingReadAccessTo (NSURL, = docDir)
0x100004f20      bl sym.imp.objc_msgSend
```

**Tracing `x3` (the `allowingReadAccessTo` argument):**

`x20` is set at `0x100004f08` by bridging a Swift lazy static `URL` to an `NSURL`. Rather than relying on the developer-assigned symbol name, we locate its initializer through the iOS API it calls: `axt @ 0x100018130` (the `URLsForDirectory:inDomains:` reloc) leads directly to `func.100004000` (the xref result is in `output.txt`; the disassembly is in `docDir-init.asm`).

In `func.100004000` (from `docDir-init.asm`), the key sequence is:

```text
0x100004070      ldr x0, [x8, 0x1a0]   ; reloc.NSFileManager class
0x100004074      bl sym.imp.objc_opt_self
0x10000407c      ldr x1, [x8, 0x128]   ; reloc.fixup.defaultManager selector
0x100004080      bl sym.imp.objc_msgSend
...
0x100004094      ldr x1, [x8, 0x130]   ; URLs