Bug 1185640 - serviceworker register() should not accept escaped slashes. r=bkelly
authorAndrea Marchesini <amarchesini@mozilla.com>
Wed, 19 Aug 2015 13:23:58 -0700
changeset 259329 7c5cc8cfca19afaeb4af08c669a78219edea72c4
parent 259328 c47ccde36664172dff25f8b444ffd7a6b150cee3
child 259330 e1d8c2010f8346935cfcfc5b6c3586b4a3efe4c8
push id29277
push userryanvm@gmail.com
push dateWed, 26 Aug 2015 18:32:23 +0000
treeherdermozilla-central@fea87cbeaa6b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbkelly
bugs1185640
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1185640 - serviceworker register() should not accept escaped slashes. r=bkelly
dom/base/nsContentUtils.h
dom/workers/ServiceWorkerContainer.cpp
dom/workers/test/serviceworkers/mochitest.ini
dom/workers/test/serviceworkers/test_escapedSlashes.html
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1691,26 +1691,22 @@ public:
    * Case insensitive comparison between two strings. However it only ignores
    * case for ASCII characters a-z.
    */
   static bool EqualsIgnoreASCIICase(const nsAString& aStr1,
                                     const nsAString& aStr2);
 
   /**
    * Convert ASCII A-Z to a-z.
-   * @return NS_OK on success, or NS_ERROR_OUT_OF_MEMORY if making the string
-   * writable needs to allocate memory and that allocation fails.
    */
   static void ASCIIToLower(nsAString& aStr);
   static void ASCIIToLower(const nsAString& aSource, nsAString& aDest);
 
   /**
    * Convert ASCII a-z to A-Z.
-   * @return NS_OK on success, or NS_ERROR_OUT_OF_MEMORY if making the string
-   * writable needs to allocate memory and that allocation fails.
    */
   static void ASCIIToUpper(nsAString& aStr);
   static void ASCIIToUpper(const nsAString& aSource, nsAString& aDest);
 
   /**
    * Return whether aStr contains an ASCII uppercase character.
    */
   static bool StringContainsASCIIUpper(const nsAString& aStr);
--- a/dom/workers/ServiceWorkerContainer.cpp
+++ b/dom/workers/ServiceWorkerContainer.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ServiceWorkerContainer.h"
 
 #include "nsIDocument.h"
 #include "nsIServiceWorkerManager.h"
+#include "nsIURL.h"
 #include "nsNetUtil.h"
 #include "nsPIDOMWindow.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 
 #include "nsCycleCollectionParticipant.h"
 #include "nsServiceManagerUtils.h"
 
@@ -95,16 +96,41 @@ ServiceWorkerContainer::RemoveReadyPromi
 }
 
 JSObject*
 ServiceWorkerContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return ServiceWorkerContainerBinding::Wrap(aCx, this, aGivenProto);
 }
 
+static nsresult
+CheckForSlashEscapedCharsInPath(nsIURI* aURI)
+{
+  MOZ_ASSERT(aURI);
+
+  nsCOMPtr<nsIURL> url(do_QueryInterface(aURI));
+  if (NS_WARN_IF(!url)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsAutoCString path;
+  nsresult rv = url->GetFilePath(path);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  ToLowerCase(path);
+  if (path.Find("%2f") != kNotFound ||
+      path.Find("%5c") != kNotFound) {
+    return NS_ERROR_DOM_TYPE_ERR;
+  }
+
+  return NS_OK;
+}
+
 already_AddRefed<Promise>
 ServiceWorkerContainer::Register(const nsAString& aScriptURL,
                                  const RegistrationOptions& aOptions,
                                  ErrorResult& aRv)
 {
   nsCOMPtr<nsISupports> promise;
 
   nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager();
@@ -139,16 +165,21 @@ ServiceWorkerContainer::Register(const n
   nsresult rv;
   nsCOMPtr<nsIURI> scriptURI;
   rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL, nullptr, baseURI);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     aRv.ThrowTypeError(MSG_INVALID_URL, &aScriptURL);
     return nullptr;
   }
 
+  aRv = CheckForSlashEscapedCharsInPath(scriptURI);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
   // In ServiceWorkerContainer.register() the scope argument is parsed against
   // different base URLs depending on whether it was passed or not.
   nsCOMPtr<nsIURI> scopeURI;
 
   // Step 4. If none passed, parse against script's URL
   if (!aOptions.mScope.WasPassed()) {
     NS_NAMED_LITERAL_STRING(defaultScope, "./");
     rv = NS_NewURI(getter_AddRefs(scopeURI), defaultScope,
@@ -166,16 +197,21 @@ ServiceWorkerContainer::Register(const n
                    nullptr, baseURI);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       nsAutoCString spec;
       baseURI->GetSpec(spec);
       NS_ConvertUTF8toUTF16 wSpec(spec);
       aRv.ThrowTypeError(MSG_INVALID_SCOPE, &aOptions.mScope.Value(), &wSpec);
       return nullptr;
     }
+
+    aRv = CheckForSlashEscapedCharsInPath(scopeURI);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return nullptr;
+    }
   }
 
   // The spec says that the "client" passed to Register() must be the global
   // where the ServiceWorkerContainer was retrieved from.
   nsCOMPtr<nsPIDOMWindow> window = GetOwner();
   MOZ_ASSERT(window);
   aRv = swm->Register(window, scopeURI, scriptURI, getter_AddRefs(promise));
   if (aRv.Failed()) {
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -242,10 +242,11 @@ skip-if = toolkit == "android" || toolki
 [test_strict_mode_warning.html]
 [test_third_party_iframes.html]
 [test_unregister.html]
 [test_workerUnregister.html]
 [test_workerUpdate.html]
 [test_workerupdatefoundevent.html]
 [test_opaque_intercept.html]
 [test_fetch_event_client_postmessage.html]
+[test_escapedSlashes.html]
 [test_eventsource_intercept.html]
 [test_not_intercept_plugin.html]
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_escapedSlashes.html
@@ -0,0 +1,102 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for escaped slashes in navigator.serviceWorker.register</title>
+  <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" />
+  <base href="https://mozilla.org/">
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test"></pre>
+<script class="testbody" type="text/javascript">
+
+var tests = [
+  { status:  true,
+    scriptURL: "a.js?foo%2fbar",
+    scopeURL: null },
+  { status:  false,
+    scriptURL: "foo%2fbar",
+    scopeURL: null },
+  { status:  true,
+    scriptURL: "a.js?foo%2Fbar",
+    scopeURL: null },
+  { status:  false,
+    scriptURL: "foo%2Fbar",
+    scopeURL: null },
+  { status:  true,
+    scriptURL: "a.js?foo%5cbar",
+    scopeURL: null },
+  { status:  false,
+    scriptURL: "foo%5cbar",
+    scopeURL: null },
+  { status:  true,
+    scriptURL: "a.js?foo%2Cbar",
+    scopeURL: null },
+  { status:  false,
+    scriptURL: "foo%5Cbar",
+    scopeURL: null },
+  { status:  true,
+    scriptURL: "ok.js",
+    scopeURL: "/scope?foo%2fbar"},
+  { status:  false,
+    scriptURL: "ok.js",
+    scopeURL: "/foo%2fbar"},
+  { status:  true,
+    scriptURL: "ok.js",
+    scopeURL: "/scope?foo%2Fbar"},
+  { status:  false,
+    scriptURL: "ok.js",
+    scopeURL: "foo%2Fbar"},
+  { status:  true,
+    scriptURL: "ok.js",
+    scopeURL: "/scope?foo%5cbar"},
+  { status:  false,
+    scriptURL: "ok.js",
+    scopeURL: "foo%5cbar"},
+  { status:  true,
+    scriptURL: "ok.js",
+    scopeURL: "/scope?foo%5Cbar"},
+  { status:  false,
+    scriptURL: "ok.js",
+    scopeURL: "foo%5Cbar"},
+];
+
+function runTest() {
+  if (!tests.length) {
+    SimpleTest.finish();
+    return;
+  }
+
+  var test = tests.shift();
+  navigator.serviceWorker.register(test.scriptURL, test.scopeURL)
+    .then(reg => {
+      ok(false, "Register should fail");
+    }, err => {
+      if (!test.status) {
+        is(err.name, "TypeError", "Registration should fail with TypeError");
+      } else {
+        ok(test.status, "Register should fail");
+      }
+    })
+    .then(runTest);
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = function() {
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+    ["dom.serviceWorkers.testing.enabled", true],
+    ["dom.serviceWorkers.enabled", true],
+  ]}, runTest);
+};
+
+</script>
+</body>
+</html>