Bug 1390801 - FeaturePolicy - part 1 - HTTP header and attribute parser, r=ckerschb
authorAndrea Marchesini <amarchesini@mozilla.com>
Mon, 01 Oct 2018 08:09:43 +0200
changeset 487264 eb62db6b03435f82ab697fc81776ccd965aa36e2
parent 487263 4e879c661c84d27e657d10425229232f9798c7cd
child 487265 8edf2b229c9c3f51f15e33169affd1e733043664
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersckerschb
bugs1390801
milestone64.0a1
Bug 1390801 - FeaturePolicy - part 1 - HTTP header and attribute parser, r=ckerschb
dom/locales/en-US/chrome/security/security.properties
dom/security/featurepolicy/Feature.cpp
dom/security/featurepolicy/Feature.h
dom/security/featurepolicy/FeaturePolicyParser.cpp
dom/security/featurepolicy/FeaturePolicyParser.h
dom/security/featurepolicy/FeaturePolicyUtils.cpp
dom/security/featurepolicy/FeaturePolicyUtils.h
dom/security/featurepolicy/moz.build
dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp
dom/security/featurepolicy/test/gtest/moz.build
dom/security/moz.build
--- a/dom/locales/en-US/chrome/security/security.properties
+++ b/dom/locales/en-US/chrome/security/security.properties
@@ -94,8 +94,12 @@ BlockSubresourceFTP=Loading FTP subresource within http(s) page not allowed (Blocked loading of: “%1$S”)
 
 # LOCALIZATION NOTE (BrowserUpgradeInsecureDisplayRequest):
 # %1$S is the browser name "brandShortName"; %2$S is the URL of the upgraded request; %1$S is the upgraded scheme.
 BrowserUpgradeInsecureDisplayRequest = %1$S is upgrading an insecure display request ‘%2$S’ to use ‘%3$S’
 # LOCALIZATION NOTE (RunningClearSiteDataValue):
 # %S is the URI of the resource whose data was cleaned up
 RunningClearSiteDataValue=Clear-Site-Data header forced the clean up of “%S” data.
 UnknownClearSiteDataValue=Clear-Site-Data header found. Unknown value “%S”.
+
+FeaturePolicyUnsupportedFeatureName=Feature Policy: Skipping unsupported feature name “%S”.
+# TODO: would be nice to add a link to the Feature-Policy MDN documentation here.
+FeaturePolicyInvalidAllowValue=Feature Policy: Skipping unsupported allow value “%S”.
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/Feature.cpp
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "Feature.h"
+
+#include "nsIURI.h"
+
+using namespace mozilla::dom;
+
+Feature::Feature(const nsAString& aFeatureName)
+  : mFeatureName(aFeatureName)
+  , mPolicy(eWhiteList)
+{}
+
+Feature::~Feature() = default;
+
+const nsAString&
+Feature::Name() const
+{
+  return mFeatureName;
+}
+
+void
+Feature::SetAllowsNone()
+{
+  mPolicy = eNone;
+  mWhiteList.Clear();
+}
+
+bool
+Feature::AllowsNone() const
+{
+  return mPolicy == eNone;
+}
+
+void
+Feature::SetAllowsAll()
+{
+  mPolicy = eAll;
+  mWhiteList.Clear();
+}
+
+bool
+Feature::AllowsAll() const
+{
+  return mPolicy == eAll;
+}
+
+void
+Feature::AppendURIToWhiteList(nsIURI* aURI)
+{
+  mPolicy = eWhiteList;
+  mWhiteList.AppendElement(aURI);
+}
+
+bool
+Feature::WhiteListContains(nsIURI* aURI) const
+{
+  MOZ_ASSERT(aURI);
+
+  if (!IsWhiteList()) {
+    return false;
+  }
+
+  bool equal = false;
+  for (nsIURI* uri : mWhiteList) {
+    if (NS_SUCCEEDED(uri->Equals(aURI, &equal)) && equal) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool
+Feature::IsWhiteList() const
+{
+  return mPolicy == eWhiteList;
+}
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/Feature.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_Feature_h
+#define mozilla_dom_Feature_h
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+
+class nsIURI;
+
+namespace mozilla {
+namespace dom {
+
+class Feature final
+{
+public:
+  explicit Feature(const nsAString& aFeatureName);
+
+  ~Feature();
+
+  const nsAString&
+  Name() const;
+
+  void
+  SetAllowsNone();
+
+  bool
+  AllowsNone() const;
+
+  void
+  SetAllowsAll();
+
+  bool
+  AllowsAll() const;
+
+  void
+  AppendURIToWhiteList(nsIURI* aURI);
+
+  bool
+  WhiteListContains(nsIURI* aURI) const;
+
+  bool
+  IsWhiteList() const;
+
+private:
+  nsString mFeatureName;
+
+  enum Policy {
+    // denotes a policy of "feature 'none'"
+    eNone,
+
+    // denotes a policy of "feature *"
+    eAll,
+
+    // denotes a policy of "feature bar.com foo.com"
+    eWhiteList,
+  };
+
+  Policy mPolicy;
+
+  nsTArray<nsCOMPtr<nsIURI>> mWhiteList;
+};
+
+} // dom namespace
+} // mozilla namespace
+
+#endif // mozilla_dom_Feature_h
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyParser.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "FeaturePolicyParser.h"
+
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/PolicyTokenizer.h"
+#include "nsIScriptError.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+namespace {
+
+void
+ReportToConsoleUnsupportedFeature(nsIDocument* aDocument,
+                                  const nsString& aFeatureName)
+{
+  const char16_t* params[] = { aFeatureName.get() };
+
+  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                  NS_LITERAL_CSTRING("Feature Policy"),
+                                  aDocument,
+                                  nsContentUtils::eSECURITY_PROPERTIES,
+                                  "FeaturePolicyUnsupportedFeatureName",
+                                  params, ArrayLength(params));
+}
+
+void
+ReportToConsoleInvalidAllowValue(nsIDocument* aDocument,
+                                 const nsString& aValue)
+{
+  const char16_t* params[] = { aValue.get() };
+
+  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                  NS_LITERAL_CSTRING("Feature Policy"),
+                                  aDocument,
+                                  nsContentUtils::eSECURITY_PROPERTIES,
+                                  "FeaturePolicyInvalidAllowValue",
+                                  params, ArrayLength(params));
+}
+
+} // anonymous
+
+/* static */ bool
+FeaturePolicyParser::ParseString(const nsAString& aPolicy,
+                                 nsIDocument* aDocument,
+                                 nsIURI* aSelfURI,
+                                 nsTArray<Feature>& aParsedFeatures)
+{
+  MOZ_ASSERT(aSelfURI);
+
+  nsTArray<nsTArray<nsString>> tokens;
+  PolicyTokenizer::tokenizePolicy(aPolicy, tokens);
+
+  nsTArray<Feature> parsedFeatures;
+
+  for (const nsTArray<nsString>& featureTokens : tokens) {
+    if (featureTokens.IsEmpty()) {
+      continue;
+    }
+
+    if (!FeaturePolicyUtils::IsSupportedFeature(featureTokens[0])) {
+      ReportToConsoleUnsupportedFeature(aDocument, featureTokens[0]);
+      continue;
+    }
+
+    Feature feature(featureTokens[0]);
+
+    // we gotta start at 1 here
+    for (uint32_t i = 1; i < featureTokens.Length(); ++i) {
+      const nsString& curVal = featureTokens[i];
+      if (curVal.LowerCaseEqualsASCII("'none'")) {
+      	feature.SetAllowsNone();
+        break;
+      }
+
+      if (curVal.EqualsLiteral("*")) {
+      	feature.SetAllowsAll();
+        break;
+      }
+
+      if (curVal.LowerCaseEqualsASCII("'self'")) {
+        feature.AppendURIToWhiteList(aSelfURI);
+        continue;
+      }
+
+      nsCOMPtr<nsIURI> uri;
+      nsresult rv = NS_NewURI(getter_AddRefs(uri), curVal);
+      if (NS_FAILED(rv)) {
+        ReportToConsoleInvalidAllowValue(aDocument, curVal);
+        continue;
+      }
+
+      feature.AppendURIToWhiteList(uri);
+    }
+
+    parsedFeatures.AppendElement(feature);
+  }
+
+  aParsedFeatures.SwapElements(parsedFeatures);
+  return true;
+}
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyParser.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_FeaturePolicyParser_h
+#define mozilla_dom_FeaturePolicyParser_h
+
+#include "nsString.h"
+
+class nsIDocument;
+class nsIURI;
+
+namespace mozilla {
+namespace dom {
+
+class Feature;
+
+class FeaturePolicyParser final
+{
+public:
+  static bool
+  ParseString(const nsAString& aPolicy,
+              nsIDocument* aDocument,
+              nsIURI* aSelfURI,
+              nsTArray<Feature>& aParsedFeatures);
+};
+
+} // dom namespace
+} // mozilla namespace
+
+#endif // mozilla_dom_FeaturePolicyParser_h
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "FeaturePolicyUtils.h"
+
+using namespace mozilla::dom;
+
+static const char* sSupportedFeatures[] = {
+  "camera",
+  "geolocation",
+  "microphone",
+};
+
+/* static */ bool
+FeaturePolicyUtils::IsSupportedFeature(const nsAString& aFeatureName)
+{
+  uint32_t numFeatures = (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
+  for (uint32_t i = 0; i < numFeatures; i++) {
+    if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i])) {
+      return true;
+    }
+  }
+  return false;
+}
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_FeaturePolicyUtils_h
+#define mozilla_dom_FeaturePolicyUtils_h
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class FeaturePolicyUtils final
+{
+public:
+  static bool
+  IsSupportedFeature(const nsAString& aFeatureName);
+};
+
+} // dom namespace
+} // mozilla namespace
+
+#endif // mozilla_dom_FeaturePolicyUtils_h
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+    BUG_COMPONENT = ('Core', 'DOM: Security')
+
+TEST_DIRS += [ 'test/gtest' ]
+
+EXPORTS.mozilla.dom += [
+  'Feature.h',
+  'FeaturePolicyParser.h',
+  'FeaturePolicyUtils.h',
+]
+
+SOURCES += [
+  'Feature.cpp',
+  'FeaturePolicyParser.cpp',
+  'FeaturePolicyUtils.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "nsNetUtil.h"
+#include "nsTArray.h"
+
+using namespace mozilla::dom;
+
+void
+CheckParser(const nsAString& aInput, bool aExpectedResults,
+            uint32_t aExpectedFeatures, nsTArray<Feature>& aParsedFeatures)
+{
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = NS_NewURI(getter_AddRefs(uri),
+                          NS_LITERAL_STRING("https://example.com"));
+  ASSERT_EQ(NS_OK, rv) << "NS_NewURI works.";
+
+  nsTArray<Feature> parsedFeatures;
+  ASSERT_TRUE(FeaturePolicyParser::ParseString(aInput, nullptr, uri,
+                                               parsedFeatures) ==
+                aExpectedResults);
+  ASSERT_TRUE(parsedFeatures.Length() == aExpectedFeatures);
+
+  parsedFeatures.SwapElements(aParsedFeatures);
+}
+
+TEST(FeaturePolicyParser, Basic)
+{
+  nsCOMPtr<nsIURI> uriSelf;
+  nsresult rv = NS_NewURI(getter_AddRefs(uriSelf),
+                          NS_LITERAL_STRING("https://example.com"));
+  ASSERT_EQ(NS_OK, rv) << "NS_NewURI works.";
+
+  nsCOMPtr<nsIURI> uriExampleCom;
+  rv = NS_NewURI(getter_AddRefs(uriExampleCom),
+                 NS_LITERAL_STRING("http://example.com"));
+  ASSERT_EQ(NS_OK, rv) << "NS_NewURI works.";
+
+  nsCOMPtr<nsIURI> uriExampleNet;
+  rv = NS_NewURI(getter_AddRefs(uriExampleNet),
+                 NS_LITERAL_STRING("http://example.net"));
+  ASSERT_EQ(NS_OK, rv) << "NS_NewURI works.";
+
+  nsTArray<Feature> parsedFeatures;
+
+  // Empty string is a valid policy.
+  CheckParser(EmptyString(), true, 0, parsedFeatures);
+
+  // Empty string with spaces is still valid.
+  CheckParser(NS_LITERAL_STRING("   "), true, 0, parsedFeatures);
+
+  // Non-Existing features with no allowed values
+  CheckParser(NS_LITERAL_STRING("non-existing-feature"), true, 0, parsedFeatures);
+  CheckParser(NS_LITERAL_STRING("non-existing-feature;another-feature"), true,
+              0, parsedFeatures);
+
+  // Existing feature with no allowed values
+  CheckParser(NS_LITERAL_STRING("camera"), true, 1, parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+
+  // Some spaces.
+  CheckParser(NS_LITERAL_STRING(" camera "), true, 1, parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+
+  // A random ;
+  CheckParser(NS_LITERAL_STRING("camera;"), true, 1, parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+
+  // Another random ;
+  CheckParser(NS_LITERAL_STRING(";camera;"), true, 1, parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+
+  // 2 features
+  CheckParser(NS_LITERAL_STRING("camera;microphone"), true, 2, parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+  ASSERT_TRUE(parsedFeatures[1].Name().Equals(NS_LITERAL_STRING("microphone")));
+  ASSERT_TRUE(parsedFeatures[1].IsWhiteList());
+
+  // 2 features with spaces
+  CheckParser(NS_LITERAL_STRING(" camera ; microphone "), true, 2,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+  ASSERT_TRUE(parsedFeatures[1].Name().Equals(NS_LITERAL_STRING("microphone")));
+  ASSERT_TRUE(parsedFeatures[1].IsWhiteList());
+
+  // 3 features, but only 2 exist.
+  CheckParser(NS_LITERAL_STRING("camera;microphone;foobar"), true, 2,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+  ASSERT_TRUE(parsedFeatures[1].Name().Equals(NS_LITERAL_STRING("microphone")));
+  ASSERT_TRUE(parsedFeatures[1].IsWhiteList());
+
+  // Multiple spaces around the value
+  CheckParser(NS_LITERAL_STRING("camera      'self'"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriSelf));
+
+  // Multiple spaces around the value
+  CheckParser(NS_LITERAL_STRING("camera      'self'    "), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriSelf));
+
+  // No final '
+  CheckParser(NS_LITERAL_STRING("camera      'self"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].IsWhiteList());
+  ASSERT_TRUE(!parsedFeatures[0].WhiteListContains(uriSelf));
+
+  // Lowercase/Uppercase
+  CheckParser(NS_LITERAL_STRING("camera      'selF'"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriSelf));
+
+  // Lowercase/Uppercase
+  CheckParser(NS_LITERAL_STRING("camera * 'self' none' a.com 123"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+
+  // After a 'none' we don't continue the parsing.
+  CheckParser(NS_LITERAL_STRING("camera 'none' a.com b.org c.net d.co.uk"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].AllowsNone());
+
+  // After a * we don't continue the parsing.
+  CheckParser(NS_LITERAL_STRING("camera * a.com b.org c.net d.co.uk"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+
+  // 'self'
+  CheckParser(NS_LITERAL_STRING("camera 'self'"), true, 1, parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriSelf));
+
+  // A couple of URLs
+  CheckParser(NS_LITERAL_STRING("camera http://example.com http://example.net"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(!parsedFeatures[0].WhiteListContains(uriSelf));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriExampleCom));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriExampleNet));
+
+  // A couple of URLs + self
+  CheckParser(NS_LITERAL_STRING("camera http://example.com 'self' http://example.net"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriSelf));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriExampleCom));
+  ASSERT_TRUE(parsedFeatures[0].WhiteListContains(uriExampleNet));
+
+  // A couple of URLs but then *
+  CheckParser(NS_LITERAL_STRING("camera http://example.com 'self' http://example.net *"), true, 1,
+              parsedFeatures);
+  ASSERT_TRUE(parsedFeatures[0].Name().Equals(NS_LITERAL_STRING("camera")));
+  ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+}
new file mode 100644
--- /dev/null
+++ b/dom/security/featurepolicy/test/gtest/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+UNIFIED_SOURCES = [
+    'TestFeaturePolicyParser.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul-gtest'
+
--- a/dom/security/moz.build
+++ b/dom/security/moz.build
@@ -4,16 +4,18 @@
 # 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/.
 
 with Files('*'):
     BUG_COMPONENT = ('Core', 'DOM: Security')
 
 TEST_DIRS += ['test']
 
+DIRS += [ 'featurepolicy' ]
+
 EXPORTS.mozilla.dom += [
     'ContentVerifier.h',
     'CSPEvalChecker.h',
     'FramingChecker.h',
     'nsContentSecurityManager.h',
     'nsCSPContext.h',
     'nsCSPService.h',
     'nsCSPUtils.h',