diff options
Diffstat (limited to '')
19 files changed, 2091 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..1b2a383642 --- /dev/null +++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp @@ -0,0 +1,309 @@ +/* -*- 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}, + {"speaker-selection", 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, ¬null)) { + 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..40277836ad --- /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.ini"] + +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.ini b/dom/security/featurepolicy/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..3c1850cc2e --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/mochitest.ini @@ -0,0 +1,11 @@ +[DEFAULT] +prefs = + dom.security.featurePolicy.header.enabled=true + dom.security.featurePolicy.webidl.enabled=true +support-files = + empty.html + test_parser.html^headers^ + +[test_parser.html] +fail-if = xorigin +[test_featureList.html] 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..377c5fccb4 --- /dev/null +++ b/dom/security/featurepolicy/test/mochitest/test_featureList.html @@ -0,0 +1,44 @@ +<!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", + "display-capture", + "document-domain", + "speaker-selection", + "vr", + "web-share", +]; + +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' |