Revision: 5698
Author:   [email protected]
Date:     Mon Sep 22 22:12:44 2014 UTC
Log: Protect service JSONP responses against "Rosetta Flash" vulnerability.
https://codereview.appspot.com/117650043

The so-called "Rosetta Flash" vulnerability is that allowing arbitrary
yet identifier-like text at the beginning of a JSONP response is
sufficient for it to be interpreted as a Flash file executing in that
origin. See for more information:
http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/

All JSONP responses from Caja services now:
* are prefixed with "/**/", which still allows them to execute as JSONP
  but removes requester control over the first bytes of the response.
* have the response header Content-Disposition: attachment.

Another recommended mitigation, "X-Content-Type-Options: nosniff", was
already present.

This change includes a backport of r5619 "Convert fetching proxy tests
to client-side tests." which was previously skipped, so as to simplify
applying the same fix to the ES5 branch.

Bug: <https://code.google.com/p/google-caja/issues/detail?id=1923>

[email protected]

https://code.google.com/p/google-caja/source/detail?r=5698

Added:
/branches/es53/tests/com/google/caja/plugin/test-fetch-proxy-fixture-unicode.ujs
 /branches/es53/tests/com/google/caja/plugin/test-fetch-proxy-fixture.css
 /branches/es53/tests/com/google/caja/plugin/test-fetch-proxy.js
Modified:
 /branches/es53/build.xml
 /branches/es53/src/com/google/caja/plugin/caja.js
 /branches/es53/src/com/google/caja/service/AbstractCajolingHandler.java
 /branches/es53/src/com/google/caja/service/CajolingServlet.java
 /branches/es53/tests/com/google/caja/plugin/browser-tests.json
 /branches/es53/tests/com/google/caja/service/ProxyHandlerTest.java
 /branches/es53/tests/com/google/caja/service/ServiceTestCase.java
 /branches/es53/tests/com/google/caja/util/LocalServer.java

=======================================
--- /dev/null
+++ /branches/es53/tests/com/google/caja/plugin/test-fetch-proxy-fixture-unicode.ujs Mon Sep 22 22:12:44 2014 UTC
@@ -0,0 +1,1 @@
+1२𐄉
=======================================
--- /dev/null
+++ /branches/es53/tests/com/google/caja/plugin/test-fetch-proxy-fixture.css Mon Sep 22 22:12:44 2014 UTC
@@ -0,0 +1,1 @@
+body {}
=======================================
--- /dev/null
+++ /branches/es53/tests/com/google/caja/plugin/test-fetch-proxy.js Mon Sep 22 22:12:44 2014 UTC
@@ -0,0 +1,205 @@
+// Copyright (C) 2013 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Tests of the fetching proxy server.
+ *
+ * @author [email protected]
+ * @requires caja, jsunitRun, readyToTest, basicCajaConfig, location
+ */
+
+(function() {
+  'use strict';
+
+ // xhr wrapper for exercising explicit requests, specialized for use in tests
+  // note uses jsunitCallback and associates with the calling test
+  function tfetch(url, expectedMimeType, callback) {
+    var request = new XMLHttpRequest();
+    request.open('GET', url.toString(), true);
+    request.onreadystatechange = jsunitCallback(function() {
+      if (request.readyState === 4) {
+        if (request.status === 200) {
+          console.log('Response: ' + request.responseText);
+          assertEquals('response Content-Type', expectedMimeType,
+              request.getResponseHeader('Content-Type').split(';')[0]);
+
+          // Rosetta Flash vuln mitigation expected
+          assertEquals('nosniff', request.getResponseHeader(
+              'X-Content-Type-Options'));
+ assertEquals('attachment; filename=f.txt', request.getResponseHeader(
+              'Content-Disposition'));
+          callback(request.responseText);
+        } else {
+          fail('Unexpected response: ' + request.statusText);
+        }
+      }
+    });
+    request.send();
+  }
+
+  var JSONP_RE = /^\/\*\*\/([a-zA-Z_]+)\((\{.*\})\);$/;
+
+  function docURL(name) {
+    return location.protocol + '//' + location.host +
+        '/ant-testlib/com/google/caja/plugin/' + name;
+  }
+
+  function docURLForURL(name) {
+    return encodeURIComponent(docURL(name));
+  }
+
+  function assertSuccessfulResponse(content, response) {
+    assertEquals('object', typeof response);
+    assertEquals(content, response.html);
+    if ('messages' in response) {
+      assertEquals(0, response.messages.length);
+    }
+  }
+
+  function assertErrorResponse(response) {
+    assertEquals('object', typeof response);
+    assertFalse('html' in response);
+    if ('messages' in response) {
+      assertTrue('error message present', response.messages.length > 0);
+    }
+  }
+
+  // --- What we're testing ---
+
+  // Note: If we need tests for a different server, add URL parameters to
+  // select it here.
+  var server = basicCajaConfig['cajaServer'];
+  var fetcher = caja.policy.net.fetcher.USE_AS_PROXY(server);
+
+
+  // TODO(kpreid): Test USE_AS_PROXY itself (e.g. exactly what URL it is
+  // constructing and sending to the server)
+
+
+  // --- Server tests ---
+ // Testing that the server behaves properly as an HTTP server independent of
+  // our client.
+
+  jsunitRegister('testServerJsonp', function() {
+    tfetch(
+ server + '/cajole?url=' + docURLForURL('test-fetch-proxy-fixture.css')
+          + '&input-mime-type=text/css'
+          + '&alt=json-in-script'
+          + '&callback=foo'
+          + '&transform=PROXY'
+          + '&build-version=' + cajaBuildVersion,
+      'text/javascript',
+      function(response) {
+        var match = JSONP_RE.exec(response);
+        assertTrue('is JSONP', !!match);
+        assertEquals('foo', match[1]);
+        assertSuccessfulResponse('body {}', JSON.parse(match[2]));
+        jsunitPass();
+      });
+  });
+
+ // TODO(kpreid): We no longer care about JSON output; remove server support
+  // for it and then remove this test
+  jsunitRegister('testServerJson', function() {
+    tfetch(
+ server + '/cajole?url=' + docURLForURL('test-fetch-proxy-fixture.css')
+          + '&input-mime-type=text/css'
+          + '&alt=json'
+          + '&callback=foo'
+          + '&transform=PROXY'
+          + '&build-version=' + cajaBuildVersion,
+      'application/json',
+      function(response) {
+        assertSuccessfulResponse('body {}', JSON.parse(response));
+        jsunitPass();
+      });
+  });
+
+  jsunitRegister('testServerJsonpAbsent', function() {
+    tfetch(
+ server + '/cajole?url=' + docURLForURL('test-fetch-proxy-nonexistent.css')
+          + '&input-mime-type=text/css'
+          + '&alt=json-in-script'
+          + '&callback=foo'
+          + '&transform=PROXY'
+          + '&build-version=' + cajaBuildVersion,
+      'text/javascript',
+      function(response) {
+        var match = JSONP_RE.exec(response);
+        assertTrue('is JSONP', !!match);
+        assertEquals('foo', match[1]);
+        assertErrorResponse(JSON.parse(match[2]));
+        jsunitPass();
+      });
+  });
+
+  jsunitRegister('testServerJsonAbsent', function() {
+    tfetch(
+ server + '/cajole?url=' + docURLForURL('test-fetch-proxy-nonexistent.css')
+          + '&input-mime-type=text/css'
+          + '&alt=json'
+          + '&callback=foo'
+          + '&transform=PROXY'
+          + '&build-version=' + cajaBuildVersion,
+      'application/json',
+      function(response) {
+        assertErrorResponse(JSON.parse(response));
+        jsunitPass();
+      });
+  });
+
+
+  // --- End-to-end tests ---
+  // Testing both proxy server behavior and the USE_AS_PROXY client glue.
+
+  jsunitRegister('testBasic', function() {
+    fetcher(docURL('test-fetch-proxy-fixture.css'), 'text/css',
+        jsunitCallback(function(response) {
+          console.log('Response:', response);
+          assertSuccessfulResponse('body {}', response);
+          jsunitPass();
+        }));
+  });
+
+  jsunitRegister('testError', function() {
+    fetcher(docURL('test-fetch-proxy-nonexistent.css'), 'text/css',
+        jsunitCallback(function(response) {
+          console.log('Response:', response);
+          assertErrorResponse(response);
+          jsunitPass();
+        }));
+  });
+
+  jsunitRegister('testUnexpectedMimeType', function() {
+    fetcher(docURL('test-fetch-proxy-fixture.css'), 'text/javascript',
+        jsunitCallback(function(response) {
+          console.log('Response:', response);
+          assertErrorResponse(response);
+          jsunitPass();
+        }));
+  });
+
+  jsunitRegister('testUnicode', function() {
+    // not actually JS, but the current fetcher only permits CSS and JS
+ fetcher(docURL('test-fetch-proxy-fixture-unicode.ujs'), 'text/javascript',
+        jsunitCallback(function(response) {
+          console.log('Response:', response);
+          assertSuccessfulResponse('1\u0968\ud800\udd09', response);
+          jsunitPass();
+        }));
+  });
+
+  readyToTest();
+  jsunitRun();
+})();
=======================================
--- /branches/es53/build.xml    Wed Feb 26 21:33:26 2014 UTC
+++ /branches/es53/build.xml    Mon Sep 22 22:12:44 2014 UTC
@@ -750,7 +750,7 @@
     <!-- These and other HTML and JS files are copied verbatim -->
     <copy todir="${testlib}">
<fileset dir="${src}" includes="**/*.html,**/*.js,**/*.css,**/*.jpg,**/*.png"/> - <fileset dir="${tests}" includes="**/*.html,**/*.js,**/*.css,**/*.jpg,**/*.png"/> + <fileset dir="${tests}" includes="**/*.html,**/*.js,**/*.css,**/*.jpg,**/*.png,**/*.ujs"/>
       <fileset dir="${src}" includes="**/apitaming/*"/>
       <fileset dir="${tests}" includes="**/apitaming/*"/>
     </copy>
=======================================
--- /branches/es53/src/com/google/caja/plugin/caja.js Tue Jan 7 21:20:26 2014 UTC +++ /branches/es53/src/com/google/caja/plugin/caja.js Mon Sep 22 22:12:44 2014 UTC
@@ -58,7 +58,7 @@
       // TODO(jasvir): Make it so this does not pollute the host page
       // namespace but rather just the loaderFrame
       installSyncScript(rndName,
-        proxyServer ? String(proxyServer) : caja['server']
+        (proxyServer ? String(proxyServer) : caja['server'])
         + '/cajole?url=' + encodeURIComponent(url.toString())
         + '&input-mime-type=' + encodeURIComponent(mime)
         + '&transform=PROXY'
=======================================
--- /branches/es53/src/com/google/caja/service/AbstractCajolingHandler.java Tue Sep 24 18:19:56 2013 UTC +++ /branches/es53/src/com/google/caja/service/AbstractCajolingHandler.java Mon Sep 22 22:12:44 2014 UTC
@@ -167,7 +167,7 @@

     output.append(
         (jsonpCallback != null)
-            ? jsonpCallback + "(" + rendered + ");"
+            ? "/**/" + jsonpCallback + "(" + rendered + ");"
             : rendered);
     output.flush();
   }
=======================================
--- /branches/es53/src/com/google/caja/service/CajolingServlet.java Wed Jun 20 23:08:49 2012 UTC +++ /branches/es53/src/com/google/caja/service/CajolingServlet.java Mon Sep 22 22:12:44 2014 UTC
@@ -187,6 +187,7 @@
       resp.setContentLength(content.length);
       resp.setHeader(UMP.a, UMP.b);
       resp.setHeader("X-Content-Type-Options", "nosniff");
+      resp.setHeader("Content-Disposition", "attachment; filename=f.txt");

       resp.getOutputStream().write(content);
       resp.getOutputStream().close();
=======================================
--- /branches/es53/tests/com/google/caja/plugin/browser-tests.json Wed Feb 26 21:33:26 2014 UTC +++ /branches/es53/tests/com/google/caja/plugin/browser-tests.json Mon Sep 22 22:12:44 2014 UTC
@@ -74,6 +74,7 @@
"expecting them should cause it to never make progress in load() or",
           "whenReady() calls."]
     },
+    { "driver": "test-fetch-proxy.js" },
     { "driver": "test-client-uri-rewriting.js" },
     { "bare": "cajajs-bare-test.html", "mode": "both" },
     { "driver": "test-automode1.js", "mode": "es53" },
=======================================
--- /branches/es53/tests/com/google/caja/service/ProxyHandlerTest.java Wed Apr 17 23:02:09 2013 UTC +++ /branches/es53/tests/com/google/caja/service/ProxyHandlerTest.java Mon Sep 22 22:12:44 2014 UTC
@@ -17,6 +17,12 @@
 import com.google.caja.reporting.BuildInfo;

 /**
+ * Note: These tests are now redundant with browser-side test
+ * .../plugin/test-fetch-proxy.js. They have been left in because there's no
+ * need to delete them and it's a little better to have Java tests for Java
+ * code, but in the event the implementation of the proxy is changed these might
+ * as well be discarded.
+ *
  * @author [email protected] (Kevin Reid)
  */
 public class ProxyHandlerTest extends ServiceTestCase {
=======================================
--- /branches/es53/tests/com/google/caja/service/ServiceTestCase.java Tue Sep 24 18:19:56 2013 UTC +++ /branches/es53/tests/com/google/caja/service/ServiceTestCase.java Mon Sep 22 22:12:44 2014 UTC
@@ -168,7 +168,7 @@
       String emitted,
       String jsonProperty,
       String... expectedSubstrings) throws Exception {
-    Pattern p = Pattern.compile("(?s)^[a-zA-Z_]+\\((\\{.*\\})\\);$");
+ Pattern p = Pattern.compile("(?s)^/\\*\\*/[a-zA-Z_]+\\((\\{.*\\})\\);$");
     Matcher m = p.matcher(emitted);
     assertTrue(m.matches());
     assertSubstringsInJson(m.group(1), jsonProperty, expectedSubstrings);
@@ -177,7 +177,8 @@
   protected static void assertCallbackInJsonp(
       String emitted,
       String jsonpCallback) throws Exception {
- Pattern p = Pattern.compile("(?s)^" + jsonpCallback + "\\((\\{.*\\})\\);$");
+    Pattern p = Pattern.compile("(?s)^/\\*\\*/" + jsonpCallback +
+        "\\((\\{.*\\})\\);$");
     Matcher m = p.matcher(emitted);
     assertTrue(m.matches());
   }
=======================================
--- /branches/es53/tests/com/google/caja/util/LocalServer.java Wed Feb 12 18:09:06 2014 UTC +++ /branches/es53/tests/com/google/caja/util/LocalServer.java Mon Sep 22 22:12:44 2014 UTC
@@ -87,6 +87,8 @@
       }
     };
     resource_handler.setResourceBase(".");
+    resource_handler.getMimeTypes().addMimeMapping(
+        "ujs", "text/javascript;charset=utf-8");

     // caja (=playground for now) server under /caja directory
     final String subdir = "/caja";

--

--- You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/d/optout.

Reply via email to