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