MASTG-DEMO-0096: HTML Injection in a Local WebView Leading to Local File Access
Download MASTG-DEMO-0096 IPA Open MASTG-DEMO-0096 Folder Build MASTG-DEMO-0096 IPA
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:).
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 Attacker-Controlled Input in a WebView Leading to Unintended Navigation 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.
- The app loads a local HTML file into a
WKWebView. - The WebView is granted read access to the entire
Documentsdirectory. - The page reads attacker controlled input from the
usernamequery parameter. - That value is inserted into the DOM using
innerHTML. - The attacker injects markup that loads another local file, such as
secret.txt. - 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
Steps¶
- Unzip the app package and locate the main binary file as described in Exploring the App Package. In this case, the binary is
./Payload/MASTestApp.app/MASTestApp. - Open the app binary with radare2 (iOS) and inspect the code paths that create and load the
WKWebView. - Identify any calls that load content into the WebView, such as
load(_:)orloadFileURL(_:allowingReadAccessTo:). - Obtain the code that updates the page content after loading, to verify whether attacker-controlled input is inserted into the DOM using
innerHTML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
1 2 | |
Observation¶
The output shows the loadFileURL:allowingReadAccessToURL: call site, the docDir and fileURL lazy initializers, and the innerHTML assignment in the embedded HTML template.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
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 Attacker-Controlled Input in a WebView Leading to Unintended Navigation for more details on the HTML injection aspect of the vulnerability.
The following analysis complements the one in Attacker-Controlled Input in a WebView Leading to Unintended Navigation by focusing on the overly broad file access aspect of the vulnerability.
AI-Decompiled Code Analysis¶
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.
- On lines 10–12,
docDiris initialized toFileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!, which resolves to the app'sDocumentsdirectory — the same directory that containssecret.txt. - On lines 14–16,
fileURLis built by appending"index.html"directly todocDir, so the loaded HTML file and any sensitive sibling files share the same parent directory. - On line 35,
webView.loadFileURL(url, allowingReadAccessTo: docDir)passesdocDiras the read-access root. BecausedocDiris the entireDocumentsdirectory, the WebView can reach any file inside it, includingsecret.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):
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:
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] ; URLsForDirectory:inDomains: selector
0x100004098 mov w2, 9
0x10000409c mov w3, 1
0x1000040a0 bl sym.imp.objc_msgSend
This is the pattern for calling:
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
because:
9isNSDocumentDirectory1isNSUserDomainMask
The function bridges the returned NSArray to a Swift array and stores its first element as the docDir lazy static. This confirms that x3 (the allowingReadAccessTo argument) is the app's Documents directory.
You can find the enum values above in /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSPathUtilities.h.
typedef NS_ENUM(NSUInteger, NSSearchPathDirectory) {
NSApplicationDirectory = 1, // supported applications (Applications)
NSDemoApplicationDirectory, // unsupported applications, demonstration versions (Demos)
NSDeveloperApplicationDirectory, // developer applications (Developer/Applications). DEPRECATED - there is no one single Developer directory.
NSAdminApplicationDirectory, // system and network administration applications (Administration)
NSLibraryDirectory, // various documentation, support, and configuration files, resources (Library)
NSDeveloperDirectory, // developer resources (Developer) DEPRECATED - there is no one single Developer directory.
NSUserDirectory, // user home directories (Users)
NSDocumentationDirectory, // documentation (Documentation)
NSDocumentDirectory, // documents (Documents)
...
typedef NS_OPTIONS(NSUInteger, NSSearchPathDomainMask) {
NSUserDomainMask = 1, // user's home directory --- place to install user's personal items (~)
NSLocalDomainMask = 2, // local to the current machine --- place to install items available to everyone on this machine (/Library)
NSNetworkDomainMask = 4, // publicly available location in the local area network --- place to install items available on the network (/Network)
NSSystemDomainMask = 8, // provided by Apple, unmodifiable (/System)
NSAllDomainsMask = 0x0ffff // all domains: all of the above and future items
};
Tracing x2 (the file URL argument):
x26 is set at 0x100004edc by bridging a Swift lazy static URL to an NSURL. We locate its initializer via the iOS API it calls: axt @ 0x10000a738 (the appendingPathComponent import stub) leads to func.10000418c (the materializer ZTm_). axt @ 0x10000418c then reveals its swift_once guard func.100004144 (Z_) (all xref results are in output.txt). Z_ encodes the filename "index.html" inline as immediate character constants (from fileURL-init.asm, which also includes ZTm_ appended after):
0x10000414c mov x2, 0x6e69 ; 'in'
0x100004150 movk x2, 0x6564, lsl 16 ; 'de'
0x100004154 movk x2, 0x2e78, lsl 32 ; 'x.'
0x100004158 movk x2, 0x7468, lsl 48 ; 'ht'
0x10000415c mov x3, 0x6c6d ; 'ml'
The ZTm_ materializer then loads the docDir lazy static and passes the encoded filename to appendingPathComponent:
0x100004200 ldr x8, [x8, 0x1b0] ; sym.MASTestApp.MastgTest.docDir._6E8AB2C58CE173A727EF27CB85DF8CD8_...z_
...
0x100004240 bl sym.imp.Foundation.URL.appendingPathComponent_...CSSF_
This confirms that fileURL is built by appending "index.html" to docDir. Therefore, the app loads index.html from the Documents directory and grants the WebView read access to the entire Documents directory.
How to Fix¶
To prevent this:
- Move
index.htmlinto a dedicated subdirectory (for example,Library/Application Support/webContent/) and pass that subdirectory asallowingReadAccessToinstead of the fullDocumentsdirectory. This ensures the WebView can't reachsecret.txtor any other user data stored inDocuments. - Replace
innerHTMLwithtextContentin the JavaScript injection so that user input is always treated as plain text, not markup.
| fix.diff | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
To apply the fix from fix.diff to the original MastgTest.swift file, run the following command from this demo directory:
patch ../MASTG-DEMO-0095/MastgTest.swift -o MastgTest-fixed.swift < fix.diff