packageorg.owasp.mastestappimportandroid.os.Bundleimportandroid.webkit.WebStorageimportandroid.webkit.WebViewimportandroidx.activity.ComponentActivityimportandroidx.activity.compose.BackHandlerimportandroidx.activity.compose.setContentimportandroidx.activity.enableEdgeToEdgeimportandroidx.compose.foundation.layout.fillMaxSizeimportandroidx.compose.foundation.layout.paddingimportandroidx.compose.foundation.shape.RoundedCornerShapeimportandroidx.compose.material3.Textimportandroidx.compose.runtime.Composableimportandroidx.compose.runtime.getValueimportandroidx.compose.runtime.keyimportandroidx.compose.runtime.mutableIntStateOfimportandroidx.compose.runtime.mutableStateOfimportandroidx.compose.runtime.rememberimportandroidx.compose.runtime.setValueimportandroidx.compose.ui.Modifierimportandroidx.compose.ui.draw.clipimportandroidx.compose.ui.graphics.Colorimportandroidx.compose.ui.platform.LocalContextimportandroidx.compose.ui.text.font.FontFamilyimportandroidx.compose.ui.tooling.preview.Previewimportandroidx.compose.ui.unit.dpimportandroidx.compose.ui.unit.spimportandroidx.compose.ui.viewinterop.AndroidViewclassMainActivityWebView:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)enableEdgeToEdge()setContent{WebViewScreen()}}overridefunonStop(){WebStorage.getInstance().deleteAllData()super.onStop()}}@Composable@PreviewfunWebViewScreen(){valcontext=LocalContext.currentvalmastgTestWebViewClass=MastgTestWebView(context)varshowWebViewbyremember{mutableStateOf(false)}// Controls whether the WebView is displayedvarwebViewKeybyremember{mutableIntStateOf(0)}// Unique key to force WebView recreationvarwebViewRefbyremember{mutableStateOf<WebView?>(null)}// Holds a reference to the WebView// Use BackHandler to intercept the back press when the WebView is visible.if(showWebView){BackHandler{// Check if the WebView can go back in history.if(webViewRef?.canGoBack()==true){webViewRef?.goBack()}else{// If no back history exists, hide the WebView.showWebView=false}}}BaseScreen(onStartClick={showWebView=true// Show the WebViewwebViewKey++// Increment key to create/recreate WebView}){if(showWebView){// Recompose AndroidView with a new key when showWebView is truekey(webViewKey){AndroidView(factory={WebView(context).apply{webViewRef=this// Store a reference to the WebView.mastgTestWebViewClass.mastgTest(this)setBackgroundColor(0)}},modifier=Modifier.fillMaxSize().clip(RoundedCornerShape(8.dp)))}}else{// Placeholder with textText(modifier=Modifier.padding(16.dp),text="Click \"Start\" to open the WebView.",color=Color.White,fontSize=20.sp,fontFamily=FontFamily.SansSerif)}}}
packageorg.owasp.mastestappimportandroid.annotation.SuppressLintimportandroid.app.Activityimportandroid.content.Contextimportandroid.graphics.Bitmapimportandroid.util.Logimportandroid.webkit.JavascriptInterfaceimportandroid.webkit.WebViewimportandroid.webkit.WebViewClientclassMastgTestWebView(privatevalcontext:Context){@SuppressLint("SetJavaScriptEnabled")funmastgTest(webView:WebView){webView.apply{// Enable JS and DOM storage so the page can use localStoragesettings.apply{javaScriptEnabled=truedomStorageEnabled=true}// Bridge object exposed to JS as "Android" so the page can request the app to closeaddJavascriptInterface(AndroidBridge(),"Android")// Inline HTML which writes sensitive data into localStorage, shows a countdown,// and calls Android.closeApp() when finished.valhtml=""" <!doctype html> <html> <head> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <style> body{ background:#111; color:#fff; font-family:sans-serif; margin:0; padding:0; text-align:center; } .msg{ font-size:20px; margin:12px; display:block; } .count{ font-size:30px; margin-top:8px; display:inline-block; } </style> </head> <body> <div class="msg">Stored a token and user's driving licence ID in Local Storage.</div> <br /> <div class="msg">The app will now close in : <span id="count" class="count"></span></div> <script> // Store sensitive data in localStorage try { localStorage.setItem('sensitive_token', 'SECRET_TOKEN_123456'); localStorage.setItem('driving_license_id', 'DL-987654321'); } catch(e) { /* ignore */ } var n = 10; var el = document.getElementById('count'); if(el) el.innerText = n; var t = setInterval(function(){ n--; if(n<=0){ clearInterval(t); if(el) el.innerText = 'Closing...'; try { Android.closeApp(); } catch(e) { /* ignore */ } } else { if(el) el.innerText = n; } }, 1000); </script> </body> </html> """.trimIndent()// Load the HTML into the WebView. Use a base URL so localStorage origin is defined.loadDataWithBaseURL("https://mas.owasp.org/",html,"text/html","utf-8",null)}}privateinnerclassAndroidBridge{@Suppress("unused")// called from JS@JavascriptInterfacefuncloseApp(){valactivity=contextas?Activity?:returnactivity.runOnUiThread{activity.finish()}}}}
Make sure you have Frida for Android installed on your machine and the frida-server running on the device
Run run.sh to spawn the app with Frida
Click the Start button in the app
Wait for the Frida script to capture the WebView cleanup calls
Stop the script by quitting the Frida CLI
Use Host-Device Data Transfer to search the contents of the /data/data/<app_package>/app_webview/ directory for the sensitive data used in the WebView.
functionenumerateDeleteAllDataMethod(){constres=Java.enumerateMethods('com.android.webview.chromium.*!deleteAllData');returnres&&res[0];}functionenumerateSetDomStorageEnabledMethod(){constres=Java.enumerateMethods('com.android.webview.chromium.*!setDomStorageEnabled');returnres&&res[0];}functionensureClassLoaded(className){try{Java.use(className);}catch(e){}}functionprintBacktrace(maxLines=8){letException=Java.use("java.lang.Exception");letstackTrace=Exception.$new().getStackTrace().toString().split(",");console.log("\nBacktrace:");for(leti=0;i<Math.min(maxLines,stackTrace.length);i++){console.log(stackTrace[i]);}console.log("\n");}Java.perform(function(){console.log('Enumerating chromium.');if(enumerateDeleteAllDataMethod()===undefined){console.log('Bring WebStorage to memory so we can hook its deleteAllData method.');Java.use('android.webkit.WebStorage').getInstance();}constdeleteAllDataMethod=enumerateDeleteAllDataMethod();if(deleteAllDataMethod!==undefined){Java.classFactory.loader=deleteAllDataMethod.loader;constWebStorageAdapter=Java.use(deleteAllDataMethod.classes[0].name);WebStorageAdapter.deleteAllData.implementation=function(){console.log('WebStorage.deleteAllData called.');printBacktrace();returnthis.deleteAllData();};console.log('WebStorage.deleteAllData hooked.');}else{console.log('WebStorage.deleteAllData not found.');}ensureClassLoaded('android.webkit.WebView');ensureClassLoaded('android.webkit.WebSettings');constdomMethod=enumerateSetDomStorageEnabledMethod();if(domMethod!==undefined){Java.classFactory.loader=domMethod.loader;constContentSettingsAdapter=Java.use(domMethod.classes[0].name);if(ContentSettingsAdapter.setDomStorageEnabled.overloads.length===0){console.log('setDomStorageEnabled found but no overloads exposed.');}else{ContentSettingsAdapter.setDomStorageEnabled.overloads.forEach(function(ov){ov.implementation=function(){constenabled=arguments.length>0?arguments[0]:undefined;console.log('ContentSettingsAdapter.setDomStorageEnabled called, enabled is '+enabled+'.');printBacktrace();returnov.apply(this,arguments);};});console.log('ContentSettingsAdapter.setDomStorageEnabled hooked.');}}else{console.log('ContentSettingsAdapter.setDomStorageEnabled not found.');}});
The test passes as the application properly cleans up all storage data from the WebView cache using the WebStorage API.
If you want to demonstrate the failure case, you can comment out the WebStorage.getInstance().deleteAllData() line in the code and rerun the test. In this case, the test fails because the sensitive data remains in the WebView storage directory after closing the app.