From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- dom/security/featurepolicy/Feature.cpp | 76 ++++ dom/security/featurepolicy/Feature.h | 65 ++++ dom/security/featurepolicy/FeaturePolicy.cpp | 334 ++++++++++++++++ dom/security/featurepolicy/FeaturePolicy.h | 204 ++++++++++ dom/security/featurepolicy/FeaturePolicyParser.cpp | 157 ++++++++ dom/security/featurepolicy/FeaturePolicyParser.h | 30 ++ dom/security/featurepolicy/FeaturePolicyUtils.cpp | 309 +++++++++++++++ dom/security/featurepolicy/FeaturePolicyUtils.h | 91 +++++ dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp | 67 ++++ dom/security/featurepolicy/fuzztest/fp_fuzzer.dict | 54 +++ dom/security/featurepolicy/fuzztest/moz.build | 18 + dom/security/featurepolicy/moz.build | 36 ++ .../test/gtest/TestFeaturePolicyParser.cpp | 162 ++++++++ dom/security/featurepolicy/test/gtest/moz.build | 13 + .../featurepolicy/test/mochitest/empty.html | 1 + .../featurepolicy/test/mochitest/mochitest.ini | 11 + .../test/mochitest/test_featureList.html | 44 +++ .../featurepolicy/test/mochitest/test_parser.html | 418 +++++++++++++++++++++ .../test/mochitest/test_parser.html^headers^ | 1 + 19 files changed, 2091 insertions(+) create mode 100644 dom/security/featurepolicy/Feature.cpp create mode 100644 dom/security/featurepolicy/Feature.h create mode 100644 dom/security/featurepolicy/FeaturePolicy.cpp create mode 100644 dom/security/featurepolicy/FeaturePolicy.h create mode 100644 dom/security/featurepolicy/FeaturePolicyParser.cpp create mode 100644 dom/security/featurepolicy/FeaturePolicyParser.h create mode 100644 dom/security/featurepolicy/FeaturePolicyUtils.cpp create mode 100644 dom/security/featurepolicy/FeaturePolicyUtils.h create mode 100644 dom/security/featurepolicy/fuzztest/fp_fuzzer.cpp create mode 100644 dom/security/featurepolicy/fuzztest/fp_fuzzer.dict create mode 100644 dom/security/featurepolicy/fuzztest/moz.build create mode 100644 dom/security/featurepolicy/moz.build create mode 100644 dom/security/featurepolicy/test/gtest/TestFeaturePolicyParser.cpp create mode 100644 dom/security/featurepolicy/test/gtest/moz.build create mode 100644 dom/security/featurepolicy/test/mochitest/empty.html create mode 100644 dom/security/featurepolicy/test/mochitest/mochitest.ini create mode 100644 dom/security/featurepolicy/test/mochitest/test_featureList.html create mode 100644 dom/security/featurepolicy/test/mochitest/test_parser.html create mode 100644 dom/security/featurepolicy/test/mochitest/test_parser.html^headers^ (limited to 'dom/security/featurepolicy') 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>& 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>& 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> 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 dest = this; + RefPtr 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> 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 aGivenProto) { + return FeaturePolicy_Binding::Wrap(aCx, this, aGivenProto); +} + +bool FeaturePolicy::AllowsFeature(const nsAString& aFeatureName, + const Optional& aOrigin) const { + nsCOMPtr origin; + if (aOrigin.WasPassed()) { + nsCOMPtr 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& aFeatures) { + RefPtr self = this; + FeaturePolicyUtils::ForEachFeature( + [self, &aFeatures](const char* aFeatureName) { + nsString featureName; + featureName.AppendASCII(aFeatureName); + aFeatures.AppendElement(featureName); + }); +} + +void FeaturePolicy::AllowedFeatures(nsTArray& aAllowedFeatures) { + RefPtr 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& 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> 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 +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 aGivenProto) override; + + nsINode* GetParentObject() const { return mParentNode; } + + // WebIDL explosed methods. + + bool AllowsFeature(const nsAString& aFeatureName, + const Optional& aOrigin) const; + + void Features(nsTArray& aFeatures); + + void AllowedFeatures(nsTArray& aAllowedFeatures); + + void GetAllowlistForFeature(const nsAString& aFeatureName, + nsTArray& aList) const; + + const nsTArray& InheritedDeniedFeatureNames() const { + return mInheritedDeniedFeatureNames; + } + + const nsTArray& AttributeEnabledFeatureNames() const { + return mAttributeEnabledFeatureNames; + } + + void SetInheritedDeniedFeatureNames( + const nsTArray& 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 mInheritedDeniedFeatureNames; + + // The list of features that have been enabled via MaybeSetAllowedPolicy. + nsTArray mAttributeEnabledFeatureNames; + + // This is set of feature names when the parent allows all for that feature. + nsTArray 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 mDeclaredFeaturesInAncestorChain; + + // Feature policy for the current context. + nsTArray mFeatures; + + // Declared string represents Feature policy. + nsString mDeclaredString; + + nsCOMPtr mDefaultOrigin; + nsCOMPtr mSelfOrigin; + nsCOMPtr 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 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 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 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& aParsedFeatures) { + MOZ_ASSERT(aSelfOrigin); + + nsTArray> tokens; + PolicyTokenizer::tokenizePolicy(aPolicy, tokens); + + nsTArray parsedFeatures; + + for (const nsTArray& 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 uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), curVal); + if (NS_FAILED(rv)) { + ReportToConsoleInvalidAllowValue(aDocument, curVal); + continue; + } + + nsCOMPtr 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& 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& 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 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 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 lineNumber; + Nullable columnNumber; + uint32_t line = 0; + uint32_t column = 0; + if (nsJSUtils::GetCallingLocation(cx, fileName, &line, &column)) { + lineNumber.SetValue(static_cast(line)); + columnNumber.SetValue(static_cast(column)); + } + + nsPIDOMWindowInner* window = aDocument->GetInnerWindow(); + if (NS_WARN_IF(!window)) { + return; + } + + RefPtr 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::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::Read( + IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr* 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 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 + +#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& 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 +struct IPDLParamTraits; + +template <> +struct IPDLParamTraits { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + mozilla::dom::FeaturePolicy* aParam); + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr* 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 selfURIPrincipal; +static nsCOMPtr 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 parsedFeatures; + + NS_ConvertASCIItoUTF16 policy(reinterpret_cast(data), size); + if (!policy.get()) return 0; + + FeaturePolicyParser::ParseString(policy, nullptr, selfURIPrincipal, + selfURIPrincipal, parsedFeatures); + + for (const Feature& feature : parsedFeatures) { + nsTArray> 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& aParsedFeatures) { + nsCOMPtr principal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF); + nsTArray parsedFeatures; + ASSERT_TRUE(FeaturePolicyParser::ParseString(aInput, nullptr, principal, + principal, parsedFeatures) == + aExpectedResults); + ASSERT_TRUE(parsedFeatures.Length() == aExpectedFeatures); + + aParsedFeatures = std::move(parsedFeatures); +} + +TEST(FeaturePolicyParser, Basic) +{ + nsCOMPtr selfPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_SELF); + nsCOMPtr exampleComPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_COM); + nsCOMPtr exampleNetPrincipal = + mozilla::BasePrincipal::CreateContentPrincipal(URL_EXAMPLE_NET); + + nsTArray 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 @@ + + + + Test feature policy - list + + + + + + + + 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 @@ + + + + Test feature policy - parsing + + + + + + + + + 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' -- cgit v1.2.3