summaryrefslogtreecommitdiffstats
path: root/dom/security/featurepolicy
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/security/featurepolicy
parentInitial commit. (diff)
downloadfirefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz
firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/security/featurepolicy')
-rw-r--r--dom/security/featurepolicy/Feature.cpp76
-rw-r--r--dom/security/featurepolicy/Feature.h65
-rw-r--r--dom/security/featurepolicy/FeaturePolicy.cpp334
-rw-r--r--dom/security/featurepolicy/FeaturePolicy.h204
-rw-r--r--dom/security/featurepolicy/FeaturePolicyParser.cpp157
-rw-r--r--dom/security/featurepolicy/FeaturePolicyParser.h30
-rw-r--r--dom/security/featurepolicy/FeaturePolicyUtils.cpp315
-rw-r--r--dom/security/featurepolicy/FeaturePolicyUtils.h91
-rw-r--r--dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp67
-rw-r--r--dom/security/featurepolicy/fuzztest/fp_fuzzer.dict54
-rw-r--r--dom/security/featurepolicy/fuzztest/moz.build18
-rw-r--r--dom/security/featurepolicy/moz.build36
-rw-r--r--dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp162
-rw-r--r--dom/security/featurepolicy/test/gtest/moz.build13
-rw-r--r--dom/security/featurepolicy/test/mochitest/empty.html1
-rw-r--r--dom/security/featurepolicy/test/mochitest/mochitest.toml14
-rw-r--r--dom/security/featurepolicy/test/mochitest/test_featureList.html48
-rw-r--r--dom/security/featurepolicy/test/mochitest/test_parser.html418
-rw-r--r--dom/security/featurepolicy/test/mochitest/test_parser.html^headers^1
19 files changed, 2104 insertions, 0 deletions
diff --git a/dom/security/featurepolicy/Feature.cpp b/dom/security/featurepolicy/Feature.cpp
new file mode 100644
index 0000000000..92a92369da
--- /dev/null
+++ b/dom/security/featurepolicy/Feature.cpp
@@ -0,0 +1,76 @@
+/* -*- 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 "mozilla/BasePrincipal.h"
+
+namespace mozilla::dom {
+
+void Feature::GetAllowList(nsTArray<nsCOMPtr<nsIPrincipal>>& aList) const {
+ MOZ_ASSERT(mPolicy == eAllowList);
+ aList.AppendElements(mAllowList);
+}
+
+bool Feature::Allows(nsIPrincipal* aPrincipal) const {
+ if (mPolicy == eNone) {
+ return false;
+ }
+
+ if (mPolicy == eAll) {
+ return true;
+ }
+
+ return AllowListContains(aPrincipal);
+}
+
+Feature::Feature(const nsAString& aFeatureName)
+ : mFeatureName(aFeatureName), mPolicy(eAllowList) {}
+
+Feature::~Feature() = default;
+
+const nsAString& Feature::Name() const { return mFeatureName; }
+
+void Feature::SetAllowsNone() {
+ mPolicy = eNone;
+ mAllowList.Clear();
+}
+
+bool Feature::AllowsNone() const { return mPolicy == eNone; }
+
+void Feature::SetAllowsAll() {
+ mPolicy = eAll;
+ mAllowList.Clear();
+}
+
+bool Feature::AllowsAll() const { return mPolicy == eAll; }
+
+void Feature::AppendToAllowList(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ mPolicy = eAllowList;
+ mAllowList.AppendElement(aPrincipal);
+}
+
+bool Feature::AllowListContains(nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT(aPrincipal);
+
+ if (!HasAllowList()) {
+ return false;
+ }
+
+ for (nsIPrincipal* principal : mAllowList) {
+ if (BasePrincipal::Cast(principal)->Subsumes(
+ aPrincipal, BasePrincipal::ConsiderDocumentDomain)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool Feature::HasAllowList() const { return mPolicy == eAllowList; }
+
+} // namespace mozilla::dom
diff --git a/dom/security/featurepolicy/Feature.h b/dom/security/featurepolicy/Feature.h
new file mode 100644
index 0000000000..e8a3d89c81
--- /dev/null
+++ b/dom/security/featurepolicy/Feature.h
@@ -0,0 +1,65 @@
+/* -*- 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 nsIPrincipal;
+
+namespace mozilla::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 AppendToAllowList(nsIPrincipal* aPrincipal);
+
+ void GetAllowList(nsTArray<nsCOMPtr<nsIPrincipal>>& aList) const;
+
+ bool AllowListContains(nsIPrincipal* aPrincipal) const;
+
+ bool HasAllowList() const;
+
+ bool Allows(nsIPrincipal* aPrincipal) 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"
+ eAllowList,
+ };
+
+ Policy mPolicy;
+
+ CopyableTArray<nsCOMPtr<nsIPrincipal>> mAllowList;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_Feature_h
diff --git a/dom/security/featurepolicy/FeaturePolicy.cpp b/dom/security/featurepolicy/FeaturePolicy.cpp
new file mode 100644
index 0000000000..cb5c1ea44a
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicy.cpp
@@ -0,0 +1,334 @@
+/* -*- 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 "FeaturePolicy.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyBinding.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsContentUtils.h"
+#include "nsNetUtil.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FeaturePolicy)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FeaturePolicy)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FeaturePolicy)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FeaturePolicy)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+FeaturePolicy::FeaturePolicy(nsINode* aNode) : mParentNode(aNode) {}
+
+void FeaturePolicy::InheritPolicy(FeaturePolicy* aParentPolicy) {
+ MOZ_ASSERT(aParentPolicy);
+
+ mInheritedDeniedFeatureNames.Clear();
+
+ RefPtr<FeaturePolicy> dest = this;
+ RefPtr<FeaturePolicy> src = aParentPolicy;
+
+ // Inherit origins which explicitly declared policy in chain
+ for (const Feature& featureInChain :
+ aParentPolicy->mDeclaredFeaturesInAncestorChain) {
+ dest->AppendToDeclaredAllowInAncestorChain(featureInChain);
+ }
+
+ FeaturePolicyUtils::ForEachFeature([dest, src](const char* aFeatureName) {
+ nsString featureName;
+ featureName.AppendASCII(aFeatureName);
+ // Store unsafe allows all (allow=*)
+ if (src->HasFeatureUnsafeAllowsAll(featureName)) {
+ dest->mParentAllowedAllFeatures.AppendElement(featureName);
+ }
+
+ // If the destination has a declared feature (via the HTTP header or 'allow'
+ // attribute) we allow the feature if the destination allows it and the
+ // parent allows its origin or the destinations' one.
+ if (dest->HasDeclaredFeature(featureName) &&
+ dest->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) {
+ if (!src->AllowsFeatureInternal(featureName, src->mDefaultOrigin) &&
+ !src->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) {
+ dest->SetInheritedDeniedFeature(featureName);
+ }
+ return;
+ }
+
+ // If there was not a declared feature, we allow the feature if the parent
+ // FeaturePolicy allows the current origin.
+ if (!src->AllowsFeatureInternal(featureName, dest->mDefaultOrigin)) {
+ dest->SetInheritedDeniedFeature(featureName);
+ }
+ });
+}
+
+void FeaturePolicy::SetInheritedDeniedFeature(const nsAString& aFeatureName) {
+ MOZ_ASSERT(!HasInheritedDeniedFeature(aFeatureName));
+ mInheritedDeniedFeatureNames.AppendElement(aFeatureName);
+}
+
+bool FeaturePolicy::HasInheritedDeniedFeature(
+ const nsAString& aFeatureName) const {
+ return mInheritedDeniedFeatureNames.Contains(aFeatureName);
+}
+
+bool FeaturePolicy::HasDeclaredFeature(const nsAString& aFeatureName) const {
+ for (const Feature& feature : mFeatures) {
+ if (feature.Name().Equals(aFeatureName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool FeaturePolicy::HasFeatureUnsafeAllowsAll(
+ const nsAString& aFeatureName) const {
+ for (const Feature& feature : mFeatures) {
+ if (feature.AllowsAll() && feature.Name().Equals(aFeatureName)) {
+ return true;
+ }
+ }
+
+ // We should look into parent too (for example, document of iframe which
+ // allows all, would be unsafe)
+ return mParentAllowedAllFeatures.Contains(aFeatureName);
+}
+
+void FeaturePolicy::AppendToDeclaredAllowInAncestorChain(
+ const Feature& aFeature) {
+ for (Feature& featureInChain : mDeclaredFeaturesInAncestorChain) {
+ if (featureInChain.Name().Equals(aFeature.Name())) {
+ MOZ_ASSERT(featureInChain.HasAllowList());
+
+ nsTArray<nsCOMPtr<nsIPrincipal>> list;
+ aFeature.GetAllowList(list);
+
+ for (nsIPrincipal* principal : list) {
+ featureInChain.AppendToAllowList(principal);
+ }
+ continue;
+ }
+ }
+
+ mDeclaredFeaturesInAncestorChain.AppendElement(aFeature);
+}
+
+bool FeaturePolicy::IsSameOriginAsSrc(nsIPrincipal* aPrincipal) const {
+ MOZ_ASSERT(aPrincipal);
+
+ if (!mSrcOrigin) {
+ return false;
+ }
+
+ return BasePrincipal::Cast(mSrcOrigin)
+ ->Subsumes(aPrincipal, BasePrincipal::ConsiderDocumentDomain);
+}
+
+void FeaturePolicy::SetDeclaredPolicy(Document* aDocument,
+ const nsAString& aPolicyString,
+ nsIPrincipal* aSelfOrigin,
+ nsIPrincipal* aSrcOrigin) {
+ ResetDeclaredPolicy();
+
+ mDeclaredString = aPolicyString;
+ mSelfOrigin = aSelfOrigin;
+ mSrcOrigin = aSrcOrigin;
+
+ Unused << NS_WARN_IF(!FeaturePolicyParser::ParseString(
+ aPolicyString, aDocument, aSelfOrigin, aSrcOrigin, mFeatures));
+
+ // Only store explicitly declared allowlist
+ for (const Feature& feature : mFeatures) {
+ if (feature.HasAllowList()) {
+ AppendToDeclaredAllowInAncestorChain(feature);
+ }
+ }
+}
+
+void FeaturePolicy::ResetDeclaredPolicy() {
+ mFeatures.Clear();
+ mDeclaredString.Truncate();
+ mSelfOrigin = nullptr;
+ mSrcOrigin = nullptr;
+ mDeclaredFeaturesInAncestorChain.Clear();
+ mAttributeEnabledFeatureNames.Clear();
+}
+
+JSObject* FeaturePolicy::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return FeaturePolicy_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool FeaturePolicy::AllowsFeature(const nsAString& aFeatureName,
+ const Optional<nsAString>& aOrigin) const {
+ nsCOMPtr<nsIPrincipal> origin;
+ if (aOrigin.WasPassed()) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aOrigin.Value());
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ origin = BasePrincipal::CreateContentPrincipal(
+ uri, BasePrincipal::Cast(mDefaultOrigin)->OriginAttributesRef());
+ } else {
+ origin = mDefaultOrigin;
+ }
+
+ if (NS_WARN_IF(!origin)) {
+ return false;
+ }
+
+ return AllowsFeatureInternal(aFeatureName, origin);
+}
+
+bool FeaturePolicy::AllowsFeatureExplicitlyInAncestorChain(
+ const nsAString& aFeatureName, nsIPrincipal* aOrigin) const {
+ MOZ_ASSERT(aOrigin);
+
+ for (const Feature& feature : mDeclaredFeaturesInAncestorChain) {
+ if (feature.Name().Equals(aFeatureName)) {
+ return feature.AllowListContains(aOrigin);
+ }
+ }
+
+ return false;
+}
+
+bool FeaturePolicy::AllowsFeatureInternal(const nsAString& aFeatureName,
+ nsIPrincipal* aOrigin) const {
+ MOZ_ASSERT(aOrigin);
+
+ // Let's see if have to disable this feature because inherited policy.
+ if (HasInheritedDeniedFeature(aFeatureName)) {
+ return false;
+ }
+
+ for (const Feature& feature : mFeatures) {
+ if (feature.Name().Equals(aFeatureName)) {
+ return feature.Allows(aOrigin);
+ }
+ }
+
+ switch (FeaturePolicyUtils::DefaultAllowListFeature(aFeatureName)) {
+ case FeaturePolicyUtils::FeaturePolicyValue::eAll:
+ return true;
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eSelf:
+ return BasePrincipal::Cast(mDefaultOrigin)
+ ->Subsumes(aOrigin, BasePrincipal::ConsiderDocumentDomain);
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eNone:
+ return false;
+
+ default:
+ MOZ_CRASH("Unknown default value");
+ }
+
+ return false;
+}
+
+void FeaturePolicy::Features(nsTArray<nsString>& aFeatures) {
+ RefPtr<FeaturePolicy> self = this;
+ FeaturePolicyUtils::ForEachFeature(
+ [self, &aFeatures](const char* aFeatureName) {
+ nsString featureName;
+ featureName.AppendASCII(aFeatureName);
+ aFeatures.AppendElement(featureName);
+ });
+}
+
+void FeaturePolicy::AllowedFeatures(nsTArray<nsString>& aAllowedFeatures) {
+ RefPtr<FeaturePolicy> self = this;
+ FeaturePolicyUtils::ForEachFeature(
+ [self, &aAllowedFeatures](const char* aFeatureName) {
+ nsString featureName;
+ featureName.AppendASCII(aFeatureName);
+
+ if (self->AllowsFeatureInternal(featureName, self->mDefaultOrigin)) {
+ aAllowedFeatures.AppendElement(featureName);
+ }
+ });
+}
+
+void FeaturePolicy::GetAllowlistForFeature(const nsAString& aFeatureName,
+ nsTArray<nsString>& aList) const {
+ if (!AllowsFeatureInternal(aFeatureName, mDefaultOrigin)) {
+ return;
+ }
+
+ for (const Feature& feature : mFeatures) {
+ if (feature.Name().Equals(aFeatureName)) {
+ if (feature.AllowsAll()) {
+ aList.AppendElement(u"*"_ns);
+ return;
+ }
+
+ nsTArray<nsCOMPtr<nsIPrincipal>> list;
+ feature.GetAllowList(list);
+
+ for (nsIPrincipal* principal : list) {
+ nsAutoCString originNoSuffix;
+ nsresult rv = principal->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ aList.AppendElement(NS_ConvertUTF8toUTF16(originNoSuffix));
+ }
+ return;
+ }
+ }
+
+ switch (FeaturePolicyUtils::DefaultAllowListFeature(aFeatureName)) {
+ case FeaturePolicyUtils::FeaturePolicyValue::eAll:
+ aList.AppendElement(u"*"_ns);
+ return;
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eSelf: {
+ nsAutoCString originNoSuffix;
+ nsresult rv = mDefaultOrigin->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ aList.AppendElement(NS_ConvertUTF8toUTF16(originNoSuffix));
+ return;
+ }
+
+ case FeaturePolicyUtils::FeaturePolicyValue::eNone:
+ return;
+
+ default:
+ MOZ_CRASH("Unknown default value");
+ }
+}
+
+void FeaturePolicy::MaybeSetAllowedPolicy(const nsAString& aFeatureName) {
+ MOZ_ASSERT(FeaturePolicyUtils::IsSupportedFeature(aFeatureName) ||
+ FeaturePolicyUtils::IsExperimentalFeature(aFeatureName));
+ // Skip if feature is in experimental phase
+ if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
+ FeaturePolicyUtils::IsExperimentalFeature(aFeatureName)) {
+ return;
+ }
+
+ if (HasDeclaredFeature(aFeatureName)) {
+ return;
+ }
+
+ Feature feature(aFeatureName);
+ feature.SetAllowsAll();
+
+ mFeatures.AppendElement(feature);
+ mAttributeEnabledFeatureNames.AppendElement(aFeatureName);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/featurepolicy/FeaturePolicy.h b/dom/security/featurepolicy/FeaturePolicy.h
new file mode 100644
index 0000000000..65f5259749
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicy.h
@@ -0,0 +1,204 @@
+/* -*- 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_FeaturePolicy_h
+#define mozilla_dom_FeaturePolicy_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPrincipal.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+/**
+ * FeaturePolicy
+ * ~~~~~~~~~~~~~
+ *
+ * Each document and each HTMLIFrameElement have a FeaturePolicy object which is
+ * used to allow or deny features in their contexts.
+ *
+ * FeaturePolicy is composed by a set of directives configured by the
+ * 'Feature-Policy' HTTP Header and the 'allow' attribute in HTMLIFrameElements.
+ * Both header and attribute are parsed by FeaturePolicyParser which returns an
+ * array of Feature objects. Each Feature object has a feature name and one of
+ * these policies:
+ * - eNone - the feature is fully disabled.
+ * - eAll - the feature is allowed.
+ * - eAllowList - the feature is allowed for a list of origins.
+ *
+ * An interesting element of FeaturePolicy is the inheritance: each context
+ * inherits the feature-policy directives from the parent context, if it exists.
+ * When a context inherits a policy for feature X, it only knows if that feature
+ * is allowed or denied (it ignores the list of allowed origins for instance).
+ * This information is stored in an array of inherited feature strings because
+ * we care only to know when they are denied.
+ *
+ * FeaturePolicy can be reset if the 'allow' or 'src' attributes change in
+ * HTMLIFrameElements. 'src' attribute is important to compute correcly
+ * the features via FeaturePolicy 'src' keyword.
+ *
+ * When FeaturePolicy must decide if feature X is allowed or denied for the
+ * current origin, it checks if the parent context denied that feature.
+ * If not, it checks if there is a Feature object for that
+ * feature named X and if the origin is allowed or not.
+ *
+ * From a C++ point of view, use FeaturePolicyUtils to obtain the list of
+ * features and to check if they are allowed in the current context.
+ *
+ * dom.security.featurePolicy.header.enabled pref can be used to disable the
+ * HTTP header support.
+ **/
+
+class nsINode;
+
+namespace mozilla::dom {
+class Document;
+class Feature;
+template <typename T>
+class Optional;
+
+class FeaturePolicyUtils;
+
+class FeaturePolicy final : public nsISupports, public nsWrapperCache {
+ friend class FeaturePolicyUtils;
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FeaturePolicy)
+
+ explicit FeaturePolicy(nsINode* aNode);
+
+ // A FeaturePolicy must have a default origin.
+ // This method must be called before any other exposed WebIDL method or before
+ // checking if a feature is allowed.
+ void SetDefaultOrigin(nsIPrincipal* aPrincipal) {
+ mDefaultOrigin = aPrincipal;
+ }
+
+ void SetSrcOrigin(nsIPrincipal* aPrincipal) { mSrcOrigin = aPrincipal; }
+
+ nsIPrincipal* DefaultOrigin() const { return mDefaultOrigin; }
+
+ // Inherits the policy from the 'parent' context if it exists.
+ void InheritPolicy(FeaturePolicy* aParentFeaturePolicy);
+
+ // Sets the declarative part of the policy. This can be from the HTTP header
+ // or for the 'allow' HTML attribute.
+ void SetDeclaredPolicy(mozilla::dom::Document* aDocument,
+ const nsAString& aPolicyString,
+ nsIPrincipal* aSelfOrigin, nsIPrincipal* aSrcOrigin);
+
+ // This method creates a policy for aFeatureName allowing it to '*' if it
+ // doesn't exist yet. It's used by HTMLIFrameElement to enable features by
+ // attributes.
+ void MaybeSetAllowedPolicy(const nsAString& aFeatureName);
+
+ // Clears all the declarative policy directives. This is needed when the
+ // 'allow' attribute or the 'src' attribute change for HTMLIFrameElement's
+ // policy.
+ void ResetDeclaredPolicy();
+
+ // This method appends a feature to in-chain declared allowlist. If the name's
+ // feature existed in the list, we only need to append the allowlist of new
+ // feature to the existed one.
+ void AppendToDeclaredAllowInAncestorChain(const Feature& aFeature);
+
+ // This method returns true if aFeatureName is declared as "*" (allow all)
+ // in parent.
+ bool HasFeatureUnsafeAllowsAll(const nsAString& aFeatureName) const;
+
+ // This method returns true if the aFeatureName is allowed for aOrigin
+ // explicitly in ancestor chain,
+ bool AllowsFeatureExplicitlyInAncestorChain(const nsAString& aFeatureName,
+ nsIPrincipal* aOrigin) const;
+
+ bool IsSameOriginAsSrc(nsIPrincipal* aPrincipal) const;
+
+ // WebIDL internal methods.
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsINode* GetParentObject() const { return mParentNode; }
+
+ // WebIDL explosed methods.
+
+ bool AllowsFeature(const nsAString& aFeatureName,
+ const Optional<nsAString>& aOrigin) const;
+
+ void Features(nsTArray<nsString>& aFeatures);
+
+ void AllowedFeatures(nsTArray<nsString>& aAllowedFeatures);
+
+ void GetAllowlistForFeature(const nsAString& aFeatureName,
+ nsTArray<nsString>& aList) const;
+
+ const nsTArray<nsString>& InheritedDeniedFeatureNames() const {
+ return mInheritedDeniedFeatureNames;
+ }
+
+ const nsTArray<nsString>& AttributeEnabledFeatureNames() const {
+ return mAttributeEnabledFeatureNames;
+ }
+
+ void SetInheritedDeniedFeatureNames(
+ const nsTArray<nsString>& aInheritedDeniedFeatureNames) {
+ mInheritedDeniedFeatureNames = aInheritedDeniedFeatureNames.Clone();
+ }
+
+ const nsAString& DeclaredString() const { return mDeclaredString; }
+
+ nsIPrincipal* GetSelfOrigin() const { return mSelfOrigin; }
+ nsIPrincipal* GetSrcOrigin() const { return mSrcOrigin; }
+
+ private:
+ ~FeaturePolicy() = default;
+
+ // This method returns true if the aFeatureName is allowed for aOrigin,
+ // following the feature-policy directives. See the comment at the top of this
+ // file.
+ bool AllowsFeatureInternal(const nsAString& aFeatureName,
+ nsIPrincipal* aOrigin) const;
+
+ // Inherits a single denied feature from the parent context.
+ void SetInheritedDeniedFeature(const nsAString& aFeatureName);
+
+ bool HasInheritedDeniedFeature(const nsAString& aFeatureName) const;
+
+ // This returns true if we have a declared feature policy for aFeatureName.
+ bool HasDeclaredFeature(const nsAString& aFeatureName) const;
+
+ nsINode* mParentNode;
+
+ // This is set in sub-contexts when the parent blocks some feature for the
+ // current context.
+ nsTArray<nsString> mInheritedDeniedFeatureNames;
+
+ // The list of features that have been enabled via MaybeSetAllowedPolicy.
+ nsTArray<nsString> mAttributeEnabledFeatureNames;
+
+ // This is set of feature names when the parent allows all for that feature.
+ nsTArray<nsString> mParentAllowedAllFeatures;
+
+ // The explicitly declared policy contains allowlist as a set of origins
+ // except 'none' and '*'. This set contains all explicitly declared policies
+ // in ancestor chain
+ nsTArray<Feature> mDeclaredFeaturesInAncestorChain;
+
+ // Feature policy for the current context.
+ nsTArray<Feature> mFeatures;
+
+ // Declared string represents Feature policy.
+ nsString mDeclaredString;
+
+ nsCOMPtr<nsIPrincipal> mDefaultOrigin;
+ nsCOMPtr<nsIPrincipal> mSelfOrigin;
+ nsCOMPtr<nsIPrincipal> mSrcOrigin;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FeaturePolicy_h
diff --git a/dom/security/featurepolicy/FeaturePolicyParser.cpp b/dom/security/featurepolicy/FeaturePolicyParser.cpp
new file mode 100644
index 0000000000..8ab95420aa
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyParser.cpp
@@ -0,0 +1,157 @@
+/* -*- 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/BasePrincipal.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"
+
+namespace mozilla::dom {
+
+namespace {
+
+void ReportToConsoleUnsupportedFeature(Document* aDocument,
+ const nsString& aFeatureName) {
+ if (!aDocument) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params = {aFeatureName};
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "Feature Policy"_ns, aDocument,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "FeaturePolicyUnsupportedFeatureName", params);
+}
+
+void ReportToConsoleInvalidEmptyAllowValue(Document* aDocument,
+ const nsString& aFeatureName) {
+ if (!aDocument) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params = {aFeatureName};
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "Feature Policy"_ns, aDocument,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "FeaturePolicyInvalidEmptyAllowValue", params);
+}
+
+void ReportToConsoleInvalidAllowValue(Document* aDocument,
+ const nsString& aValue) {
+ if (!aDocument) {
+ return;
+ }
+
+ AutoTArray<nsString, 1> params = {aValue};
+
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+ "Feature Policy"_ns, aDocument,
+ nsContentUtils::eSECURITY_PROPERTIES,
+ "FeaturePolicyInvalidAllowValue", params);
+}
+
+} // namespace
+
+/* static */
+bool FeaturePolicyParser::ParseString(const nsAString& aPolicy,
+ Document* aDocument,
+ nsIPrincipal* aSelfOrigin,
+ nsIPrincipal* aSrcOrigin,
+ nsTArray<Feature>& aParsedFeatures) {
+ MOZ_ASSERT(aSelfOrigin);
+
+ nsTArray<CopyableTArray<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]);
+
+ if (featureTokens.Length() == 1) {
+ if (aSrcOrigin) {
+ feature.AppendToAllowList(aSrcOrigin);
+ } else {
+ ReportToConsoleInvalidEmptyAllowValue(aDocument, featureTokens[0]);
+ continue;
+ }
+ } else {
+ // 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.AppendToAllowList(aSelfOrigin);
+ continue;
+ }
+
+ if (aSrcOrigin && curVal.LowerCaseEqualsASCII("'src'")) {
+ feature.AppendToAllowList(aSrcOrigin);
+ continue;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), curVal);
+ if (NS_FAILED(rv)) {
+ ReportToConsoleInvalidAllowValue(aDocument, curVal);
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> origin = BasePrincipal::CreateContentPrincipal(
+ uri, BasePrincipal::Cast(aSelfOrigin)->OriginAttributesRef());
+ if (NS_WARN_IF(!origin)) {
+ ReportToConsoleInvalidAllowValue(aDocument, curVal);
+ continue;
+ }
+
+ feature.AppendToAllowList(origin);
+ }
+ }
+
+ // No duplicate!
+ bool found = false;
+ for (const Feature& parsedFeature : parsedFeatures) {
+ if (parsedFeature.Name() == feature.Name()) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ parsedFeatures.AppendElement(feature);
+ }
+ }
+
+ aParsedFeatures = std::move(parsedFeatures);
+ return true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/security/featurepolicy/FeaturePolicyParser.h b/dom/security/featurepolicy/FeaturePolicyParser.h
new file mode 100644
index 0000000000..a60a391a78
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyParser.h
@@ -0,0 +1,30 @@
+/* -*- 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 nsIPrincipal;
+
+namespace mozilla::dom {
+
+class Document;
+class Feature;
+
+class FeaturePolicyParser final {
+ public:
+ // aSelfOrigin must not be null. if aSrcOrigin is null, the parsing will not
+ // support 'src' as valid allow directive value.
+ static bool ParseString(const nsAString& aPolicy, Document* aDocument,
+ nsIPrincipal* aSelfOrigin, nsIPrincipal* aSrcOrigin,
+ nsTArray<Feature>& aParsedFeatures);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_FeaturePolicyParser_h
diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.cpp b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
new file mode 100644
index 0000000000..4e0bb92e0a
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
@@ -0,0 +1,315 @@
+/* -*- 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"
+#include "nsIOService.h"
+
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/ipc/IPDLParamTraits.h"
+#include "mozilla/dom/FeaturePolicyViolationReportBody.h"
+#include "mozilla/dom/ReportingUtils.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsJSUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+struct FeatureMap {
+ const char* mFeatureName;
+ FeaturePolicyUtils::FeaturePolicyValue mDefaultAllowList;
+};
+
+/*
+ * IMPORTANT: Do not change this list without review from a DOM peer _AND_ a
+ * DOM Security peer!
+ */
+static FeatureMap sSupportedFeatures[] = {
+ {"camera", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"geolocation", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"microphone", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"display-capture", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"fullscreen", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"web-share", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"gamepad", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"publickey-credentials-create",
+ FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"publickey-credentials-get",
+ FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"speaker-selection", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"storage-access", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"screen-wake-lock", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+};
+
+/*
+ * This is experimental features list, which is disabled by default by pref
+ * dom.security.featurePolicy.experimental.enabled.
+ */
+static FeatureMap sExperimentalFeatures[] = {
+ // We don't support 'autoplay' for now, because it would be overwrote by
+ // 'user-gesture-activation' policy. However, we can still keep it in the
+ // list as we might start supporting it after we use different autoplay
+ // policy.
+ {"autoplay", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"encrypted-media", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"midi", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+ {"payment", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"document-domain", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ {"vr", FeaturePolicyUtils::FeaturePolicyValue::eAll},
+ // https://immersive-web.github.io/webxr/#feature-policy
+ {"xr-spatial-tracking", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
+};
+
+/* static */
+bool FeaturePolicyUtils::IsExperimentalFeature(const nsAString& aFeatureName) {
+ uint32_t numFeatures =
+ (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(
+ sExperimentalFeatures[i].mFeatureName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* 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].mFeatureName)) {
+ return true;
+ }
+ }
+
+ return StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
+ IsExperimentalFeature(aFeatureName);
+}
+
+/* static */
+void FeaturePolicyUtils::ForEachFeature(
+ const std::function<void(const char*)>& aCallback) {
+ uint32_t numFeatures =
+ (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ aCallback(sSupportedFeatures[i].mFeatureName);
+ }
+
+ if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) {
+ numFeatures =
+ (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ aCallback(sExperimentalFeatures[i].mFeatureName);
+ }
+ }
+}
+
+/* static */ FeaturePolicyUtils::FeaturePolicyValue
+FeaturePolicyUtils::DefaultAllowListFeature(const nsAString& aFeatureName) {
+ uint32_t numFeatures =
+ (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) {
+ return sSupportedFeatures[i].mDefaultAllowList;
+ }
+ }
+
+ if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) {
+ numFeatures =
+ (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
+ for (uint32_t i = 0; i < numFeatures; ++i) {
+ if (aFeatureName.LowerCaseEqualsASCII(
+ sExperimentalFeatures[i].mFeatureName)) {
+ return sExperimentalFeatures[i].mDefaultAllowList;
+ }
+ }
+ }
+
+ return FeaturePolicyValue::eNone;
+}
+
+static bool IsSameOriginAsTop(Document* aDocument) {
+ MOZ_ASSERT(aDocument);
+
+ BrowsingContext* browsingContext = aDocument->GetBrowsingContext();
+ if (!browsingContext) {
+ return false;
+ }
+
+ nsPIDOMWindowOuter* topWindow = browsingContext->Top()->GetDOMWindow();
+ if (!topWindow) {
+ // If we don't have a DOMWindow, We are not in same origin.
+ return false;
+ }
+
+ Document* topLevelDocument = topWindow->GetExtantDoc();
+ if (!topLevelDocument) {
+ return false;
+ }
+
+ return NS_SUCCEEDED(
+ nsContentUtils::CheckSameOrigin(topLevelDocument, aDocument));
+}
+
+/* static */
+bool FeaturePolicyUtils::IsFeatureUnsafeAllowedAll(
+ Document* aDocument, const nsAString& aFeatureName) {
+ MOZ_ASSERT(aDocument);
+
+ if (!aDocument->IsHTMLDocument()) {
+ return false;
+ }
+
+ FeaturePolicy* policy = aDocument->FeaturePolicy();
+ MOZ_ASSERT(policy);
+
+ return policy->HasFeatureUnsafeAllowsAll(aFeatureName) &&
+ !policy->IsSameOriginAsSrc(aDocument->NodePrincipal()) &&
+ !policy->AllowsFeatureExplicitlyInAncestorChain(
+ aFeatureName, policy->DefaultOrigin()) &&
+ !IsSameOriginAsTop(aDocument);
+}
+
+/* static */
+bool FeaturePolicyUtils::IsFeatureAllowed(Document* aDocument,
+ const nsAString& aFeatureName) {
+ MOZ_ASSERT(aDocument);
+
+ // Skip apply features in experimental phase
+ if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
+ IsExperimentalFeature(aFeatureName)) {
+ return true;
+ }
+
+ FeaturePolicy* policy = aDocument->FeaturePolicy();
+ MOZ_ASSERT(policy);
+
+ if (policy->AllowsFeatureInternal(aFeatureName, policy->DefaultOrigin())) {
+ return true;
+ }
+
+ ReportViolation(aDocument, aFeatureName);
+ return false;
+}
+
+/* static */
+void FeaturePolicyUtils::ReportViolation(Document* aDocument,
+ const nsAString& aFeatureName) {
+ MOZ_ASSERT(aDocument);
+
+ nsCOMPtr<nsIURI> uri = aDocument->GetDocumentURI();
+ if (NS_WARN_IF(!uri)) {
+ return;
+ }
+
+ // Strip the URL of any possible username/password and make it ready to be
+ // presented in the UI.
+ nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri);
+ nsAutoCString spec;
+ nsresult rv = exposableURI->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (NS_WARN_IF(!cx)) {
+ return;
+ }
+
+ nsAutoString fileName;
+ Nullable<int32_t> lineNumber;
+ Nullable<int32_t> columnNumber;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ if (nsJSUtils::GetCallingLocation(cx, fileName, &line, &column)) {
+ lineNumber.SetValue(static_cast<int32_t>(line));
+ columnNumber.SetValue(static_cast<int32_t>(column));
+ }
+
+ nsPIDOMWindowInner* window = aDocument->GetInnerWindow();
+ if (NS_WARN_IF(!window)) {
+ return;
+ }
+
+ RefPtr<FeaturePolicyViolationReportBody> body =
+ new FeaturePolicyViolationReportBody(window->AsGlobal(), aFeatureName,
+ fileName, lineNumber, columnNumber,
+ u"enforce"_ns);
+
+ ReportingUtils::Report(window->AsGlobal(), nsGkAtoms::featurePolicyViolation,
+ u"default"_ns, NS_ConvertUTF8toUTF16(spec), body);
+}
+
+} // namespace dom
+
+namespace ipc {
+void IPDLParamTraits<dom::FeaturePolicy*>::Write(IPC::MessageWriter* aWriter,
+ IProtocol* aActor,
+ dom::FeaturePolicy* aParam) {
+ if (!aParam) {
+ WriteIPDLParam(aWriter, aActor, false);
+ return;
+ }
+
+ WriteIPDLParam(aWriter, aActor, true);
+
+ dom::FeaturePolicyInfo info;
+ info.defaultOrigin() = aParam->DefaultOrigin();
+ info.selfOrigin() = aParam->GetSelfOrigin();
+ info.srcOrigin() = aParam->GetSrcOrigin();
+
+ info.declaredString() = aParam->DeclaredString();
+ info.inheritedDeniedFeatureNames() =
+ aParam->InheritedDeniedFeatureNames().Clone();
+ info.attributeEnabledFeatureNames() =
+ aParam->AttributeEnabledFeatureNames().Clone();
+
+ WriteIPDLParam(aWriter, aActor, info);
+}
+
+bool IPDLParamTraits<dom::FeaturePolicy*>::Read(
+ IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<dom::FeaturePolicy>* aResult) {
+ *aResult = nullptr;
+ bool notnull = false;
+ if (!ReadIPDLParam(aReader, aActor, &notnull)) {
+ return false;
+ }
+
+ if (!notnull) {
+ return true;
+ }
+
+ dom::FeaturePolicyInfo info;
+ if (!ReadIPDLParam(aReader, aActor, &info)) {
+ return false;
+ }
+
+ // Note that we only do IPC for feature policy to inherit policy from parent
+ // to child document. That does not need to bind feature policy with a node.
+ RefPtr<dom::FeaturePolicy> featurePolicy = new dom::FeaturePolicy(nullptr);
+ featurePolicy->SetDefaultOrigin(info.defaultOrigin());
+ featurePolicy->SetInheritedDeniedFeatureNames(
+ info.inheritedDeniedFeatureNames());
+
+ const auto& declaredString = info.declaredString();
+ if (info.selfOrigin() && !declaredString.IsEmpty()) {
+ featurePolicy->SetDeclaredPolicy(nullptr, declaredString, info.selfOrigin(),
+ info.srcOrigin());
+ }
+
+ for (auto& featureName : info.attributeEnabledFeatureNames()) {
+ featurePolicy->MaybeSetAllowedPolicy(featureName);
+ }
+
+ *aResult = std::move(featurePolicy);
+ return true;
+}
+} // namespace ipc
+
+} // namespace mozilla
diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.h b/dom/security/featurepolicy/FeaturePolicyUtils.h
new file mode 100644
index 0000000000..380806433d
--- /dev/null
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.h
@@ -0,0 +1,91 @@
+/* -*- 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 <functional>
+
+#include "mozilla/dom/FeaturePolicy.h"
+
+class PickleIterator;
+
+namespace IPC {
+class Message;
+class MessageReader;
+class MessageWriter;
+} // namespace IPC
+
+namespace mozilla {
+namespace dom {
+
+class Document;
+
+class FeaturePolicyUtils final {
+ public:
+ enum FeaturePolicyValue {
+ // Feature always allowed.
+ eAll,
+
+ // Feature allowed for documents that are same-origin with this one.
+ eSelf,
+
+ // Feature denied.
+ eNone,
+ };
+
+ // This method returns true if aFeatureName is allowed for aDocument.
+ // Use this method everywhere you need to check feature-policy directives.
+ static bool IsFeatureAllowed(Document* aDocument,
+ const nsAString& aFeatureName);
+
+ // Returns true if aFeatureName is a known feature policy name.
+ static bool IsSupportedFeature(const nsAString& aFeatureName);
+
+ // Returns true if aFeatureName is a experimental feature policy name.
+ static bool IsExperimentalFeature(const nsAString& aFeatureName);
+
+ // Runs aCallback for each known feature policy, with the feature name as
+ // argument.
+ static void ForEachFeature(const std::function<void(const char*)>& aCallback);
+
+ // Returns the default policy value for aFeatureName.
+ static FeaturePolicyValue DefaultAllowListFeature(
+ const nsAString& aFeatureName);
+
+ // This method returns true if aFeatureName is in unsafe allowed "*" case.
+ // We are in "unsafe" case when there is 'allow "*"' presents for an origin
+ // that's not presented in the ancestor feature policy chain, via src, via
+ // explicitly listed in allow, and not being the top-level origin.
+ static bool IsFeatureUnsafeAllowedAll(Document* aDocument,
+ const nsAString& aFeatureName);
+
+ private:
+ static void ReportViolation(Document* aDocument,
+ const nsAString& aFeatureName);
+};
+
+} // namespace dom
+
+namespace ipc {
+
+class IProtocol;
+
+template <typename T>
+struct IPDLParamTraits;
+
+template <>
+struct IPDLParamTraits<mozilla::dom::FeaturePolicy*> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ mozilla::dom::FeaturePolicy* aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<mozilla::dom::FeaturePolicy>* aResult);
+};
+} // namespace ipc
+} // namespace mozilla
+
+#endif // mozilla_dom_FeaturePolicyUtils_h
diff --git a/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp b/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp
new file mode 100644
index 0000000000..25f7dc8d41
--- /dev/null
+++ b/dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp
@@ -0,0 +1,67 @@
+/* -*- 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 https://mozilla.org/MPL/2.0/. */
+
+#include "FuzzingInterface.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "nsNetUtil.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static nsCOMPtr<nsIPrincipal> selfURIPrincipal;
+static nsCOMPtr<nsIURI> selfURI;
+
+static int LVVMFuzzerInitTest(int* argc, char*** argv) {
+ nsresult ret;
+ ret = NS_NewURI(getter_AddRefs(selfURI), "http://selfuri.com");
+ if (ret != NS_OK) {
+ MOZ_CRASH("NS_NewURI failed.");
+ }
+
+ mozilla::OriginAttributes attrs;
+ selfURIPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(selfURI, attrs);
+ if (!selfURIPrincipal) {
+ MOZ_CRASH("CreateContentPrincipal failed.");
+ }
+ return 0;
+}
+
+static int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ if (!size) {
+ return 0;
+ }
+ nsTArray<Feature> parsedFeatures;
+
+ NS_ConvertASCIItoUTF16 policy(reinterpret_cast<const char*>(data), size);
+ if (!policy.get()) return 0;
+
+ FeaturePolicyParser::ParseString(policy, nullptr, selfURIPrincipal,
+ selfURIPrincipal, parsedFeatures);
+
+ for (const Feature& feature : parsedFeatures) {
+ nsTArray<nsCOMPtr<nsIPrincipal>> list;
+ feature.GetAllowList(list);
+
+ for (nsIPrincipal* principal : list) {
+ nsAutoCString originNoSuffix;
+ nsresult rv = principal->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return 0;
+ }
+ printf("%s - %s\n", NS_ConvertUTF16toUTF8(feature.Name()).get(),
+ originNoSuffix.get());
+ }
+ }
+ return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(LVVMFuzzerInitTest, LLVMFuzzerTestOneInput,
+ FeaturePolicyParser);
diff --git a/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict b/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict
new file mode 100644
index 0000000000..e95508bf8e
--- /dev/null
+++ b/dom/security/featurepolicy/fuzztest/fp_fuzzer.dict
@@ -0,0 +1,54 @@
+# tokens
+"'"
+";"
+
+### https://www.w3.org/TR/{CSP,CSP2,CSP3}/
+# directive names
+"accelerometer"
+"ambient-light-sensor"
+"autoplay"
+"battery"
+"camera"
+"display-capture"
+"document-domain"
+"encrypted-media"
+"execution-while-not-rendered"
+"execution-while-out-of-viewport"
+"fullscreen
+"geolocation
+"gyroscope"
+"layout-animations"
+"legacy-image-formats"
+"magnetometer"
+"microphone"
+"midi"
+"navigation-override"
+"oversized-images"
+"payment"
+"picture-in-picture"
+"publickey-credentials"
+"sync-xhr"
+"usb"
+"vr"
+"wake-lock"
+"xr-spatial-tracking"
+
+# directive values
+"'self'"
+"'none'"
+"'src''"
+*
+
+
+# URI components
+"https:"
+"ws:"
+"blob:"
+"data:"
+"filesystem:"
+"javascript:"
+"http://"
+"selfuri.com"
+"127.0.0.1"
+"::1"
+https://example.com \ No newline at end of file
diff --git a/dom/security/featurepolicy/fuzztest/moz.build b/dom/security/featurepolicy/fuzztest/moz.build
new file mode 100644
index 0000000000..ea577e8339
--- /dev/null
+++ b/dom/security/featurepolicy/fuzztest/moz.build
@@ -0,0 +1,18 @@
+# -*- 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/.
+
+Library("FuzzingFeaturePolicy")
+
+LOCAL_INCLUDES += [
+ "/dom/security/featurepolicy",
+ "/netwerk/base",
+]
+
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+SOURCES += ["fp_fuzzer.cpp"]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/security/featurepolicy/moz.build b/dom/security/featurepolicy/moz.build
new file mode 100644
index 0000000000..b39cdd9c7f
--- /dev/null
+++ b/dom/security/featurepolicy/moz.build
@@ -0,0 +1,36 @@
+# -*- 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"]
+MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.toml"]
+
+EXPORTS.mozilla.dom += [
+ "Feature.h",
+ "FeaturePolicy.h",
+ "FeaturePolicyParser.h",
+ "FeaturePolicyUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "Feature.cpp",
+ "FeaturePolicy.cpp",
+ "FeaturePolicyParser.cpp",
+ "FeaturePolicyUtils.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/base",
+]
+include("/ipc/chromium/chromium-config.mozbuild")
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["FUZZING_INTERFACES"]:
+ TEST_DIRS += ["fuzztest"]
diff --git a/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp
new file mode 100644
index 0000000000..3e58971c9b
--- /dev/null
+++ b/dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp
@@ -0,0 +1,162 @@
+/* -*- 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/BasePrincipal.h"
+#include "mozilla/dom/Feature.h"
+#include "mozilla/dom/FeaturePolicyParser.h"
+#include "nsNetUtil.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+#define URL_SELF "https://example.com"_ns
+#define URL_EXAMPLE_COM "http://example.com"_ns
+#define URL_EXAMPLE_NET "http://example.net"_ns
+
+void CheckParser(const nsAString& aInput, bool aExpectedResults,
+ uint32_t aExpectedFeatures,
+ nsTArray<Feature>& aParsedFeatures) {
+ nsCOMPtr<nsIPrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF);
+ nsTArray<Feature> parsedFeatures;
+ ASSERT_TRUE(FeaturePolicyParser::ParseString(aInput, nullptr, principal,
+ principal, parsedFeatures) ==
+ aExpectedResults);
+ ASSERT_TRUE(parsedFeatures.Length() == aExpectedFeatures);
+
+ aParsedFeatures = std::move(parsedFeatures);
+}
+
+TEST(FeaturePolicyParser, Basic)
+{
+ nsCOMPtr<nsIPrincipal> selfPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF);
+ nsCOMPtr<nsIPrincipal> exampleComPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_COM);
+ nsCOMPtr<nsIPrincipal> exampleNetPrincipal =
+ mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_NET);
+
+ nsTArray<Feature> parsedFeatures;
+
+ // Empty string is a valid policy.
+ CheckParser(u""_ns, true, 0, parsedFeatures);
+
+ // Empty string with spaces is still valid.
+ CheckParser(u" "_ns, true, 0, parsedFeatures);
+
+ // Non-Existing features with no allowed values
+ CheckParser(u"non-existing-feature"_ns, true, 0, parsedFeatures);
+ CheckParser(u"non-existing-feature;another-feature"_ns, true, 0,
+ parsedFeatures);
+
+ // Existing feature with no allowed values
+ CheckParser(u"camera"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // Some spaces.
+ CheckParser(u" camera "_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // A random ;
+ CheckParser(u"camera;"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // Another random ;
+ CheckParser(u";camera;"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+
+ // 2 features
+ CheckParser(u"camera;microphone"_ns, true, 2, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns));
+ ASSERT_TRUE(parsedFeatures[1].HasAllowList());
+
+ // 2 features with spaces
+ CheckParser(u" camera ; microphone "_ns, true, 2, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns));
+ ASSERT_TRUE(parsedFeatures[1].HasAllowList());
+
+ // 3 features, but only 2 exist.
+ CheckParser(u"camera;microphone;foobar"_ns, true, 2, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(parsedFeatures[1].Name().Equals(u"microphone"_ns));
+ ASSERT_TRUE(parsedFeatures[1].HasAllowList());
+
+ // Multiple spaces around the value
+ CheckParser(u"camera 'self'"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // Multiple spaces around the value
+ CheckParser(u"camera 'self' "_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // No final '
+ CheckParser(u"camera 'self"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].HasAllowList());
+ ASSERT_TRUE(!parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // Lowercase/Uppercase
+ CheckParser(u"camera 'selF'"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // Lowercase/Uppercase
+ CheckParser(u"camera * 'self' none' a.com 123"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+
+ // After a 'none' we don't continue the parsing.
+ CheckParser(u"camera 'none' a.com b.org c.net d.co.uk"_ns, true, 1,
+ parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsNone());
+
+ // After a * we don't continue the parsing.
+ CheckParser(u"camera * a.com b.org c.net d.co.uk"_ns, true, 1,
+ parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+
+ // 'self'
+ CheckParser(u"camera 'self'"_ns, true, 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+
+ // A couple of URLs
+ CheckParser(u"camera http://example.com http://example.net"_ns, true, 1,
+ parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(!parsedFeatures[0].AllowListContains(selfPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleComPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleNetPrincipal));
+
+ // A couple of URLs + self
+ CheckParser(u"camera http://example.com 'self' http://example.net"_ns, true,
+ 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(selfPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleComPrincipal));
+ ASSERT_TRUE(parsedFeatures[0].AllowListContains(exampleNetPrincipal));
+
+ // A couple of URLs but then *
+ CheckParser(u"camera http://example.com 'self' http://example.net *"_ns, true,
+ 1, parsedFeatures);
+ ASSERT_TRUE(parsedFeatures[0].Name().Equals(u"camera"_ns));
+ ASSERT_TRUE(parsedFeatures[0].AllowsAll());
+}
diff --git a/dom/security/featurepolicy/test/gtest/moz.build b/dom/security/featurepolicy/test/gtest/moz.build
new file mode 100644
index 0000000000..e307810ff2
--- /dev/null
+++ b/dom/security/featurepolicy/test/gtest/moz.build
@@ -0,0 +1,13 @@
+# -*- 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"
diff --git a/dom/security/featurepolicy/test/mochitest/empty.html b/dom/security/featurepolicy/test/mochitest/empty.html
new file mode 100644
index 0000000000..64355e7d19
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/empty.html
@@ -0,0 +1 @@
+Nothing here
diff --git a/dom/security/featurepolicy/test/mochitest/mochitest.toml b/dom/security/featurepolicy/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..c621331596
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/mochitest.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+prefs = [
+ "dom.security.featurePolicy.header.enabled=true",
+ "dom.security.featurePolicy.webidl.enabled=true",
+]
+support-files = [
+ "empty.html",
+ "test_parser.html^headers^",
+]
+
+["test_featureList.html"]
+
+["test_parser.html"]
+fail-if = ["xorigin"]
diff --git a/dom/security/featurepolicy/test/mochitest/test_featureList.html b/dom/security/featurepolicy/test/mochitest/test_featureList.html
new file mode 100644
index 0000000000..8a518da653
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/test_featureList.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test feature policy - list</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe src="empty.html" id="ifr"></iframe>
+<script type="text/javascript">
+
+let supportedFeatures = [
+ "autoplay",
+ "camera",
+ "encrypted-media",
+ "fullscreen",
+ "gamepad",
+ "geolocation",
+ "microphone",
+ "midi",
+ "payment",
+ "publickey-credentials-create",
+ "publickey-credentials-get",
+ "storage-access",
+ "display-capture",
+ "document-domain",
+ "speaker-selection",
+ "vr",
+ "web-share",
+ "screen-wake-lock",
+];
+
+function checkFeatures(features) {
+ features.forEach(feature => {
+ ok(supportedFeatures.includes(feature), "Feature: " + feature);
+ });
+}
+
+ok("featurePolicy" in document, "We have document.featurePolicy");
+checkFeatures(document.featurePolicy.features());
+
+let ifr = document.getElementById("ifr");
+ok("featurePolicy" in ifr, "We have HTMLIFrameElement.featurePolicy");
+checkFeatures(ifr.featurePolicy.features());
+
+</script>
+</body>
+</html>
diff --git a/dom/security/featurepolicy/test/mochitest/test_parser.html b/dom/security/featurepolicy/test/mochitest/test_parser.html
new file mode 100644
index 0000000000..a8322f6e7d
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/test_parser.html
@@ -0,0 +1,418 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test feature policy - parsing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe src="empty.html" id="ifr"></iframe>
+<iframe src="https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html" id="cross_ifr"></iframe>
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const CROSS_ORIGIN = "https://example.org";
+
+function test_document() {
+ info("Checking document.featurePolicy");
+ ok("featurePolicy" in document, "We have document.featurePolicy");
+
+ ok(!document.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(document.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(document.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is always allowed");
+ let allowed = document.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], "*", "allowlist is *");
+
+ ok(document.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(document.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = document.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is self");
+
+ ok(!document.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is allowed for example.com");
+ ok(document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is allowed for example.org");
+ allowed = document.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = document.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ allowed = document.featurePolicy.allowedFeatures();
+ // microphone is disabled for this origin, vr is disabled everywhere.
+ let camera = false;
+ let geolocation = false;
+ allowed.forEach(a => {
+ if (a == "camera") camera = true;
+ if (a == "geolocation") geolocation = true;
+ });
+
+ ok(camera, "Camera is always allowed");
+ ok(geolocation, "Geolocation is allowed only for self");
+
+ next();
+}
+
+function test_iframe_without_allow() {
+ info("Checking HTMLIFrameElement.featurePolicy");
+ let ifr = document.getElementById("ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(ifr.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("camera", location.origin), "Camera is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], location.origin, "allowlist is 'self'");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is '*'");
+
+ ok(!ifr.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is disabled for example.org");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed");
+ // microphone is disabled for this origin
+ ok(!ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is not allowed");
+ // vr is disabled everywhere.
+ ok(!ifr.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+
+ next();
+}
+
+function test_iframe_with_allow() {
+ info("Checking HTMLIFrameElement.featurePolicy");
+ let ifr = document.getElementById("ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ifr.setAttribute("allow", "camera 'none'");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(!ifr.featurePolicy.allowsFeature("camera"), "Camera is not allowed");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 0, "Camera has an empty allowlist");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is '*'");
+
+ ok(!ifr.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is disabled for example.org");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self");
+
+ next();
+}
+
+function test_iframe_contentDocument() {
+ info("Checking iframe document.featurePolicy");
+
+ let ifr = document.createElement("iframe");
+ ifr.setAttribute("src", "empty.html");
+ ifr.onload = function() {
+ ok("featurePolicy" in ifr.contentDocument, "We have ifr.contentDocument.featurePolicy");
+
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(ifr.contentDocument.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(ifr.contentDocument.featurePolicy.allowsFeature("camera", location.origin), "Camera is allowed for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is allowed for self");
+ let allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], location.origin, "allowlist is 'self'");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", location.origin), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], location.origin, "allowlist is '*'");
+
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone"), "Microphone is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", location.origin), "Microphone is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is allowed for example.com");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is allowed for example.org");
+ allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 0, "No allowlist for microphone");
+
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr", location.origin), "Vibrate is disabled for self");
+ ok(!ifr.contentDocument.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.contentDocument.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.contentDocument.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ ok(ifr.contentDocument.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed");
+ // microphone is disabled for this origin
+ ok(!ifr.contentDocument.featurePolicy.allowedFeatures().includes("microphone"), "Microphone is not allowed");
+ // vr is disabled everywhere.
+ ok(!ifr.contentDocument.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+
+ next();
+ };
+ document.body.appendChild(ifr);
+}
+
+function test_cross_iframe_without_allow() {
+ info("Checking cross HTMLIFrameElement.featurePolicy no allow");
+ let ifr = document.getElementById("cross_ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(ifr.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("camera", CROSS_ORIGIN), "Camera is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is 'self'");
+
+ ok(!ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is not allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", CROSS_ORIGIN),
+ "Geolocation is not allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 0, "No allowlist for geolocation");
+
+ ok(ifr.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ ok(ifr.featurePolicy.allowsFeature("microphone", CROSS_ORIGIN), "Microphone is enabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is self");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", CROSS_ORIGIN), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ ok(!ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is not allowed");
+ // microphone is enabled for this origin
+ ok(ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+ // vr is disabled everywhere.
+ ok(!ifr.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+
+ next();
+}
+
+function test_cross_iframe_with_allow() {
+ info("Checking cross HTMLIFrameElement.featurePolicy with allow");
+ let ifr = document.getElementById("cross_ifr");
+ ok("featurePolicy" in ifr, "HTMLIFrameElement.featurePolicy exists");
+
+ ifr.setAttribute("allow", "geolocation; camera 'none'");
+
+ ok(!ifr.featurePolicy.allowsFeature("foobar"), "Random feature");
+ ok(!ifr.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ ok(!ifr.featurePolicy.allowsFeature("camera"), "Camera is not allowed");
+ let allowed = ifr.featurePolicy.getAllowlistForFeature("camera");
+ is(allowed.length, 0, "Camera has an empty allowlist");
+
+ ok(ifr.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ ok(ifr.featurePolicy.allowsFeature("geolocation", CROSS_ORIGIN), "Geolocation is allowed for self");
+ ok(!ifr.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("geolocation");
+ is(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is '*'");
+
+ ok(ifr.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ ok(ifr.featurePolicy.allowsFeature("microphone", CROSS_ORIGIN), "Microphone is enabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ ok(!ifr.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("microphone");
+ is(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ is(allowed[0], CROSS_ORIGIN, "allowlist is self");
+
+ ok(!ifr.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", CROSS_ORIGIN), "Vibrate is disabled for self");
+ ok(!ifr.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = ifr.featurePolicy.getAllowlistForFeature("vr");
+ is(allowed.length, 0, "No allowlist for vr");
+
+ ok(ifr.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self");
+ // microphone is enabled for this origin
+ ok(ifr.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+
+ next();
+}
+
+function test_cross_iframe_contentDocument_no_allow() {
+ info("Checking cross iframe document.featurePolicy no allow");
+
+ let ifr = document.createElement("iframe");
+ ifr.setAttribute("src", "https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html");
+ ifr.onload = async function() {
+ await SpecialPowers.spawn(ifr, [], () => {
+ Assert.ok("featurePolicy" in this.content.document, "We have this.content.document.featurePolicy");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar"), "Random feature");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("camera"), "Camera is allowed for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("camera", "https://example.org"), "Camera is allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("camera", "https://foo.bar"), "Camera is not allowed for a random URL");
+ let allowed = this.content.document.featurePolicy.getAllowlistForFeature("camera");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for camera");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is 'self'");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation"), "Geolocation is not allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://example.org"),
+ "Geolocation is not allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("geolocation");
+ Assert.equal(allowed.length, 0, "No allowlist for geolocation");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is enabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("microphone");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is self");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://example.org"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("vr");
+ Assert.equal(allowed.length, 0, "No allowlist for vr");
+
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("camera"), "Camera is allowed");
+ Assert.ok(!this.content.document.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is not allowed");
+ // microphone is enabled for this origin
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+ // vr is disabled everywhere.
+ Assert.ok(!this.content.document.featurePolicy.allowedFeatures().includes("vr"), "VR is not allowed");
+ });
+
+ next();
+ };
+ document.body.appendChild(ifr);
+}
+
+function test_cross_iframe_contentDocument_allow() {
+ info("Checking cross iframe document.featurePolicy with allow");
+
+ let ifr = document.createElement("iframe");
+ ifr.setAttribute("src", "https://example.org/tests/dom/security/featurePolicy/test/mochitest/empty.html");
+ ifr.setAttribute("allow", "geolocation; camera 'none'");
+ ifr.onload = async function() {
+ await SpecialPowers.spawn(ifr, [], () => {
+ Assert.ok("featurePolicy" in this.content.document, "We have this.content.document.featurePolicy");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar"), "Random feature");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("foobar", "https://www.something.net"), "Random feature");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("camera"), "Camera is not allowed");
+ let allowed = this.content.document.featurePolicy.getAllowlistForFeature("camera");
+ Assert.equal(allowed.length, 0, "Camera has an empty allowlist");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("geolocation"), "Geolocation is allowed for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("geolocation", "https://example.org"), "Geolocation is allowed for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("geolocation", "https://foo.bar"), "Geolocation is not allowed for any random URL");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("geolocation");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for geolocation");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is '*'");
+
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone"), "Microphone is enabled for self");
+ Assert.ok(this.content.document.featurePolicy.allowsFeature("microphone", "https://example.org"), "Microphone is enabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://foo.bar"), "Microphone is disabled for foo.bar");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("microphone", "https://example.com"), "Microphone is disabled for example.com");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("microphone");
+ Assert.equal(allowed.length, 1, "Only 1 entry in allowlist for microphone");
+ Assert.equal(allowed[0], "https://example.org", "allowlist is self");
+
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://example.org"), "Vibrate is disabled for self");
+ Assert.ok(!this.content.document.featurePolicy.allowsFeature("vr", "https://foo.bar"), "Vibrate is disabled for foo.bar");
+ allowed = this.content.document.featurePolicy.getAllowlistForFeature("vr");
+ Assert.equal(allowed.length, 0, "No allowlist for vr");
+
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("geolocation"), "Geolocation is allowed only for self");
+ // microphone is enabled for this origin
+ Assert.ok(this.content.document.featurePolicy.allowedFeatures().includes("microphone"), "microphone is allowed");
+ });
+
+ next();
+ };
+ document.body.appendChild(ifr);
+}
+
+
+var tests = [
+ test_document,
+ test_iframe_without_allow,
+ test_iframe_with_allow,
+ test_iframe_contentDocument,
+ test_cross_iframe_without_allow,
+ test_cross_iframe_with_allow,
+ test_cross_iframe_contentDocument_no_allow,
+ test_cross_iframe_contentDocument_allow
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+next();
+
+</script>
+</body>
+</html>
diff --git a/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^ b/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^
new file mode 100644
index 0000000000..949de013d3
--- /dev/null
+++ b/dom/security/featurepolicy/test/mochitest/test_parser.html^headers^
@@ -0,0 +1 @@
+Feature-Policy: camera *; geolocation 'self'; microphone https://example.com https://example.org; vr 'none'