diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/core/feature_policy')
14 files changed, 1423 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/feature_policy/BUILD.gn b/chromium/third_party/blink/renderer/core/feature_policy/BUILD.gn new file mode 100644 index 00000000000..ff3dd58753f --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/BUILD.gn @@ -0,0 +1,16 @@ +# Copyright 2018 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//third_party/blink/renderer/core/core.gni") + +blink_core_sources("feature_policy") { + sources = [ + "document_policy.h", + "feature_policy.cc", + "feature_policy.h", + "iframe_policy.h", + "policy.cc", + "policy.h", + ] +} diff --git a/chromium/third_party/blink/renderer/core/feature_policy/DEPS b/chromium/third_party/blink/renderer/core/feature_policy/DEPS new file mode 100644 index 00000000000..2b1b586d7c7 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/DEPS @@ -0,0 +1,4 @@ +include_rules = [ + # Feature policy needs to construct url::Origin objects + "+url", +] diff --git a/chromium/third_party/blink/renderer/core/feature_policy/OWNERS b/chromium/third_party/blink/renderer/core/feature_policy/OWNERS new file mode 100644 index 00000000000..329373c8198 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/OWNERS @@ -0,0 +1 @@ +file://third_party/blink/common/feature_policy/OWNERS diff --git a/chromium/third_party/blink/renderer/core/feature_policy/README.md b/chromium/third_party/blink/renderer/core/feature_policy/README.md new file mode 100644 index 00000000000..c35a27cca0b --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/README.md @@ -0,0 +1,21 @@ +This directory contains the renderer-specific portions of the [Feature +Policy API](https://wicg.github.io/feature-policy/). + +This includes: + +* The parser for the HTTP Feature-Policy header and the iframe allow attribute. + +* Helpers for manipulating the parsed declarations. + +* Implementation of the `document.policy` and `iframe.policy` interfaces. + + +## Other feature policy resources + +* The core feature policy algorithms can be found in `/common/feature\_policy/`. + +* The feature list enum is found in `/public/mojom/feature\_policy/`. + +* The recommended API for checking whether features should be enabled or not is +Document::IsFeatureEnabled() (or SecurityContext::IsFeatureEnabled in a non- +document context). diff --git a/chromium/third_party/blink/renderer/core/feature_policy/document_policy.h b/chromium/third_party/blink/renderer/core/feature_policy/document_policy.h new file mode 100644 index 00000000000..3743ef9b32f --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/document_policy.h @@ -0,0 +1,39 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_DOCUMENT_POLICY_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_DOCUMENT_POLICY_H_ + +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/feature_policy/policy.h" +#include "third_party/blink/renderer/platform/heap/member.h" + +namespace blink { + +// DocumentPolicy inherits Policy. It represents the feature policy +// introspection of a document. +class CORE_EXPORT DocumentPolicy final : public Policy { + public: + // Create a new DocumentPolicy, which is associated with |document|. + explicit DocumentPolicy(Document* document) : document_(document) {} + + void Trace(blink::Visitor* visitor) override { + visitor->Trace(document_); + ScriptWrappable::Trace(visitor); + } + + protected: + const FeaturePolicy* GetPolicy() const override { + return document_->GetFeaturePolicy(); + } + Document* GetDocument() const override { return document_; } + + private: + Member<Document> document_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_DOCUMENT_POLICY_H_ diff --git a/chromium/third_party/blink/renderer/core/feature_policy/feature_policy.cc b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy.cc new file mode 100644 index 00000000000..40161ba7252 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy.cc @@ -0,0 +1,293 @@ +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/feature_policy/feature_policy.h" + +#include <algorithm> + +#include "base/metrics/histogram_macros.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/bit_vector.h" +#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" +#include "url/origin.h" + +namespace blink { + +ParsedFeaturePolicy ParseFeaturePolicyHeader( + const String& policy, + scoped_refptr<const SecurityOrigin> origin, + Vector<String>* messages) { + return ParseFeaturePolicy(policy, origin, nullptr, messages, + GetDefaultFeatureNameMap()); +} + +ParsedFeaturePolicy ParseFeaturePolicyAttribute( + const String& policy, + scoped_refptr<const SecurityOrigin> self_origin, + scoped_refptr<const SecurityOrigin> src_origin, + Vector<String>* messages) { + return ParseFeaturePolicy(policy, self_origin, src_origin, messages, + GetDefaultFeatureNameMap()); +} + +ParsedFeaturePolicy ParseFeaturePolicy( + const String& policy, + scoped_refptr<const SecurityOrigin> self_origin, + scoped_refptr<const SecurityOrigin> src_origin, + Vector<String>* messages, + const FeatureNameMap& feature_names) { + ParsedFeaturePolicy allowlists; + BitVector features_specified( + static_cast<int>(mojom::FeaturePolicyFeature::kMaxValue)); + + // RFC2616, section 4.2 specifies that headers appearing multiple times can be + // combined with a comma. Walk the header string, and parse each comma + // separated chunk as a separate header. + Vector<String> policy_items; + // policy_items = [ policy *( "," [ policy ] ) ] + policy.Split(',', policy_items); + for (const String& item : policy_items) { + Vector<String> entry_list; + // entry_list = [ entry *( ";" [ entry ] ) ] + item.Split(';', entry_list); + for (const String& entry : entry_list) { + // Split removes extra whitespaces by default + // "name value1 value2" or "name". + Vector<String> tokens; + entry.Split(' ', tokens); + // Empty policy. Skip. + if (tokens.IsEmpty()) + continue; + if (!feature_names.Contains(tokens[0])) { + if (messages) + messages->push_back("Unrecognized feature: '" + tokens[0] + "'."); + continue; + } + + mojom::FeaturePolicyFeature feature = feature_names.at(tokens[0]); + // If a policy has already been specified for the current feature, drop + // the new policy. + if (features_specified.QuickGet(static_cast<int>(feature))) + continue; + + // Count the use of this feature policy. + if (!src_origin) { + UMA_HISTOGRAM_ENUMERATION("Blink.UseCounter.FeaturePolicy.Header", + feature); + } + + ParsedFeaturePolicyDeclaration allowlist; + allowlist.feature = feature; + features_specified.QuickSet(static_cast<int>(feature)); + std::vector<url::Origin> origins; + // If a policy entry has no (optional) values (e,g, + // allow="feature_name1; feature_name2 value"), enable the feature for: + // a. |self_origin|, if we are parsing a header policy (i.e., + // |src_origin| is null); + // b. |src_origin|, if we are parsing an allow attribute (i.e., + // |src_origin| is not null), |src_origin| is not opaque; or + // c. the opaque origin of the frame, if |src_origin| is opaque. + if (tokens.size() == 1) { + if (!src_origin) { + origins.push_back(self_origin->ToUrlOrigin()); + } else if (!src_origin->IsOpaque()) { + origins.push_back(src_origin->ToUrlOrigin()); + } else { + allowlist.matches_opaque_src = true; + } + } + + for (wtf_size_t i = 1; i < tokens.size(); i++) { + if (!tokens[i].ContainsOnlyASCII()) { + messages->push_back("Non-ASCII characters in origin."); + continue; + } + if (EqualIgnoringASCIICase(tokens[i], "'self'")) { + origins.push_back(self_origin->ToUrlOrigin()); + } else if (src_origin && EqualIgnoringASCIICase(tokens[i], "'src'")) { + // Only the iframe allow attribute can define |src_origin|. + // When parsing feature policy header, 'src' is disallowed and + // |src_origin| = nullptr. + // If the iframe will have an opaque origin (for example, if it is + // sandboxed, or has a data: URL), then 'src' needs to refer to the + // opaque origin of the frame, which is not known yet. In this case, + // the |matches_opaque_src| flag on the declaration is set, rather + // than adding an origin to the allowlist. + if (src_origin->IsOpaque()) { + allowlist.matches_opaque_src = true; + } else { + origins.push_back(src_origin->ToUrlOrigin()); + } + } else if (EqualIgnoringASCIICase(tokens[i], "'none'")) { + continue; + } else if (tokens[i] == "*") { + allowlist.matches_all_origins = true; + break; + } else { + scoped_refptr<SecurityOrigin> target_origin = + SecurityOrigin::CreateFromString(tokens[i]); + if (!target_origin->IsOpaque()) + origins.push_back(target_origin->ToUrlOrigin()); + else if (messages) + messages->push_back("Unrecognized origin: '" + tokens[i] + "'."); + } + } + allowlist.origins = origins; + allowlists.push_back(allowlist); + } + } + return allowlists; +} + +bool IsFeatureDeclared(mojom::FeaturePolicyFeature feature, + const ParsedFeaturePolicy& policy) { + return std::any_of(policy.begin(), policy.end(), + [feature](const auto& declaration) { + return declaration.feature == feature; + }); +} + +bool RemoveFeatureIfPresent(mojom::FeaturePolicyFeature feature, + ParsedFeaturePolicy& policy) { + auto new_end = std::remove_if(policy.begin(), policy.end(), + [feature](const auto& declaration) { + return declaration.feature == feature; + }); + if (new_end == policy.end()) + return false; + policy.erase(new_end, policy.end()); + return true; +} + +bool DisallowFeatureIfNotPresent(mojom::FeaturePolicyFeature feature, + ParsedFeaturePolicy& policy) { + if (IsFeatureDeclared(feature, policy)) + return false; + ParsedFeaturePolicyDeclaration allowlist; + allowlist.feature = feature; + allowlist.matches_all_origins = false; + allowlist.matches_opaque_src = false; + policy.push_back(allowlist); + return true; +} + +bool AllowFeatureEverywhereIfNotPresent(mojom::FeaturePolicyFeature feature, + ParsedFeaturePolicy& policy) { + if (IsFeatureDeclared(feature, policy)) + return false; + ParsedFeaturePolicyDeclaration allowlist; + allowlist.feature = feature; + allowlist.matches_all_origins = true; + allowlist.matches_opaque_src = true; + policy.push_back(allowlist); + return true; +} + +void DisallowFeature(mojom::FeaturePolicyFeature feature, + ParsedFeaturePolicy& policy) { + RemoveFeatureIfPresent(feature, policy); + DisallowFeatureIfNotPresent(feature, policy); +} + +void AllowFeatureEverywhere(mojom::FeaturePolicyFeature feature, + ParsedFeaturePolicy& policy) { + RemoveFeatureIfPresent(feature, policy); + AllowFeatureEverywhereIfNotPresent(feature, policy); +} + +// This method defines the feature names which will be recognized by the parser +// for the Feature-Policy HTTP header and the <iframe> "allow" attribute, as +// well as the features which will be recognized by the document or iframe +// policy object. +// +// Features which are implemented behind a flag should generally also have the +// same flag controlling whether they are in this map. Note that features which +// are shipping as part of an origin trial should add their feature names to +// this map unconditionally, as the trial token could be added after the HTTP +// header needs to be parsed. This also means that top-level documents which +// simply want to embed another page which uses an origin trial feature, without +// using the feature themselves, can use feature policy to allow use of the +// feature in subframes. (The framed document will still require a valid origin +// trial token to use the feature in this scenario.) +const FeatureNameMap& GetDefaultFeatureNameMap() { + DEFINE_STATIC_LOCAL(FeatureNameMap, default_feature_name_map, ()); + if (default_feature_name_map.IsEmpty()) { + default_feature_name_map.Set("autoplay", + mojom::FeaturePolicyFeature::kAutoplay); + default_feature_name_map.Set("camera", + mojom::FeaturePolicyFeature::kCamera); + default_feature_name_map.Set("encrypted-media", + mojom::FeaturePolicyFeature::kEncryptedMedia); + default_feature_name_map.Set("fullscreen", + mojom::FeaturePolicyFeature::kFullscreen); + default_feature_name_map.Set("geolocation", + mojom::FeaturePolicyFeature::kGeolocation); + default_feature_name_map.Set("microphone", + mojom::FeaturePolicyFeature::kMicrophone); + default_feature_name_map.Set("midi", + mojom::FeaturePolicyFeature::kMidiFeature); + default_feature_name_map.Set("speaker", + mojom::FeaturePolicyFeature::kSpeaker); + default_feature_name_map.Set("sync-xhr", + mojom::FeaturePolicyFeature::kSyncXHR); + // Under origin trial: Should be made conditional on WebVR and WebXR + // runtime flags once it is out of trial. + default_feature_name_map.Set("vr", mojom::FeaturePolicyFeature::kWebVr); + if (RuntimeEnabledFeatures::ExperimentalProductivityFeaturesEnabled()) { + default_feature_name_map.Set("animations", + mojom::FeaturePolicyFeature::kAnimations); + default_feature_name_map.Set("document-write", + mojom::FeaturePolicyFeature::kDocumentWrite); + default_feature_name_map.Set( + "image-compression", mojom::FeaturePolicyFeature::kImageCompression); + default_feature_name_map.Set("lazyload", + mojom::FeaturePolicyFeature::kLazyLoad); + default_feature_name_map.Set( + "legacy-image-formats", + mojom::FeaturePolicyFeature::kLegacyImageFormats); + default_feature_name_map.Set( + "max-downscaling-image", + mojom::FeaturePolicyFeature::kMaxDownscalingImage); + default_feature_name_map.Set("unsized-media", + mojom::FeaturePolicyFeature::kUnsizedMedia); + default_feature_name_map.Set( + "vertical-scroll", mojom::FeaturePolicyFeature::kVerticalScroll); + default_feature_name_map.Set("sync-script", + mojom::FeaturePolicyFeature::kSyncScript); + } + if (RuntimeEnabledFeatures::PaymentRequestEnabled()) { + default_feature_name_map.Set("payment", + mojom::FeaturePolicyFeature::kPayment); + } + if (RuntimeEnabledFeatures::PictureInPictureAPIEnabled()) { + default_feature_name_map.Set( + "picture-in-picture", mojom::FeaturePolicyFeature::kPictureInPicture); + } + if (RuntimeEnabledFeatures::SensorEnabled()) { + default_feature_name_map.Set("accelerometer", + mojom::FeaturePolicyFeature::kAccelerometer); + default_feature_name_map.Set( + "ambient-light-sensor", + mojom::FeaturePolicyFeature::kAmbientLightSensor); + default_feature_name_map.Set("gyroscope", + mojom::FeaturePolicyFeature::kGyroscope); + default_feature_name_map.Set("magnetometer", + mojom::FeaturePolicyFeature::kMagnetometer); + } + if (RuntimeEnabledFeatures::WebUSBEnabled()) { + default_feature_name_map.Set("usb", mojom::FeaturePolicyFeature::kUsb); + } + } + return default_feature_name_map; +} + +const String& GetNameForFeature(mojom::FeaturePolicyFeature feature) { + for (const auto& entry : GetDefaultFeatureNameMap()) { + if (entry.value == feature) + return entry.key; + } + return g_empty_string; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/feature_policy/feature_policy.h b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy.h new file mode 100644 index 00000000000..b0b71c79165 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy.h @@ -0,0 +1,95 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_FEATURE_POLICY_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_FEATURE_POLICY_H_ + +#include "base/memory/scoped_refptr.h" +#include "third_party/blink/public/common/feature_policy/feature_policy.h" +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/hash_map.h" +#include "third_party/blink/renderer/platform/wtf/text/string_hash.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +#include <memory> + +namespace blink { + +// Returns a map between feature name (string) and mojom::FeaturePolicyFeature +// (enum). +typedef HashMap<String, mojom::FeaturePolicyFeature> FeatureNameMap; +CORE_EXPORT const FeatureNameMap& GetDefaultFeatureNameMap(); + +// Converts a header policy string into a vector of allowlists, one for each +// feature specified. Unrecognized features are filtered out. If |messages| +// is not null, then any message in the input will cause a warning message to be +// appended to it. +// Example of a feature policy string: +// "vibrate a.com b.com; fullscreen 'none'; payment 'self', payment *". +CORE_EXPORT ParsedFeaturePolicy +ParseFeaturePolicyHeader(const String& policy, + scoped_refptr<const SecurityOrigin>, + Vector<String>* messages); + +// Converts a container policy string into a vector of allowlists, given self +// and src origins provided, one for each feature specified. Unrecognized +// features are filtered out. If |messages| is not null, then any message in the +// input will cause as warning message to be appended to it. +// Example of a feature policy string: +// "vibrate a.com 'src'; fullscreen 'none'; payment 'self', payment *". +CORE_EXPORT ParsedFeaturePolicy +ParseFeaturePolicyAttribute(const String& policy, + scoped_refptr<const SecurityOrigin> self_origin, + scoped_refptr<const SecurityOrigin> src_origin, + Vector<String>* messages); + +// Converts a feature policy string into a vector of allowlists (see comments +// above), with an explicit FeatureNameMap. This algorithm is called by both +// header policy parsing and container policy parsing. |self_origin| and +// |src_origin| are both nullable. +CORE_EXPORT ParsedFeaturePolicy +ParseFeaturePolicy(const String& policy, + scoped_refptr<const SecurityOrigin> self_origin, + scoped_refptr<const SecurityOrigin> src_origin, + Vector<String>* messages, + const FeatureNameMap& feature_names); + +// Returns true iff any declaration in the policy is for the given feature. +CORE_EXPORT bool IsFeatureDeclared(mojom::FeaturePolicyFeature, + const ParsedFeaturePolicy&); + +// Removes any declaration in the policy for the given feature. Returns true if +// the policy was modified. +CORE_EXPORT bool RemoveFeatureIfPresent(mojom::FeaturePolicyFeature, + ParsedFeaturePolicy&); + +// If no declaration in the policy exists already for the feature, adds a +// declaration which disallows the feature in all origins. Returns true if the +// policy was modified. +CORE_EXPORT bool DisallowFeatureIfNotPresent(mojom::FeaturePolicyFeature, + ParsedFeaturePolicy&); + +// If no declaration in the policy exists already for the feature, adds a +// declaration which allows the feature in all origins. Returns true if the +// policy was modified. +CORE_EXPORT bool AllowFeatureEverywhereIfNotPresent(mojom::FeaturePolicyFeature, + ParsedFeaturePolicy&); + +// Replaces any existing declarations in the policy for the given feature with +// a declaration which disallows the feature in all origins. +CORE_EXPORT void DisallowFeature(mojom::FeaturePolicyFeature, + ParsedFeaturePolicy&); + +// Replaces any existing declarations in the policy for the given feature with +// a declaration which allows the feature in all origins. +CORE_EXPORT void AllowFeatureEverywhere(mojom::FeaturePolicyFeature, + ParsedFeaturePolicy&); + +CORE_EXPORT const String& GetNameForFeature(mojom::FeaturePolicyFeature); + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_FEATURE_POLICY_H_ diff --git a/chromium/third_party/blink/renderer/core/feature_policy/feature_policy_fuzzer.cc b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy_fuzzer.cc new file mode 100644 index 00000000000..89610c88b14 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy_fuzzer.cc @@ -0,0 +1,26 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/feature_policy/feature_policy.h" + +#include <stddef.h> +#include <stdint.h> +#include <memory> +#include "third_party/blink/renderer/platform/heap/handle.h" +#include "third_party/blink/renderer/platform/testing/blink_fuzzer_test_support.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + static blink::BlinkFuzzerTestSupport test_support = + blink::BlinkFuzzerTestSupport(); + WTF::Vector<WTF::String> messages; + // TODO(csharrison): Be smarter about parsing this origin for performance. + scoped_refptr<const blink::SecurityOrigin> origin = + blink::SecurityOrigin::CreateFromString("https://example.com/"); + blink::ParseFeaturePolicyHeader(WTF::String(data, size), origin.get(), + &messages); + return 0; +} diff --git a/chromium/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc new file mode 100644 index 00000000000..b9c847ce617 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/feature_policy_test.cc @@ -0,0 +1,542 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/feature_policy/feature_policy.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/platform/testing/histogram_tester.h" +#include "url/gurl.h" +#include "url/origin.h" + +// Origin strings used for tests +#define ORIGIN_A "https://example.com/" +#define ORIGIN_B "https://example.net/" +#define ORIGIN_C "https://example.org/" + +class GURL; + +namespace blink { + +namespace { + +const char* const kValidPolicies[] = { + "", // An empty policy. + " ", // An empty policy. + ";;", // Empty policies. + ",,", // Empty policies. + " ; ;", // Empty policies. + " , ,", // Empty policies. + ",;,", // Empty policies. + "geolocation 'none'", + "geolocation 'self'", + "geolocation 'src'", // Only valid for iframe allow attribute. + "geolocation", // Only valid for iframe allow attribute. + "geolocation; fullscreen; payment", + "geolocation *", + "geolocation " ORIGIN_A "", + "geolocation " ORIGIN_B "", + "geolocation " ORIGIN_A " " ORIGIN_B "", + "geolocation 'none' " ORIGIN_A " " ORIGIN_B "", + "geolocation " ORIGIN_A " 'none' " ORIGIN_B "", + "geolocation 'none' 'none' 'none'", + "geolocation " ORIGIN_A " *", + "fullscreen " ORIGIN_A "; payment 'self'", + "fullscreen " ORIGIN_A "; payment *, geolocation 'self'"}; + +const char* const kInvalidPolicies[] = { + "badfeaturename", + "badfeaturename 'self'", + "1.0", + "geolocation data://badorigin", + "geolocation https://bad;origin", + "geolocation https:/bad,origin", + "geolocation https://example.com, https://a.com", + "geolocation *, payment data://badorigin", + "geolocation ws://xn--fd\xbcwsw3taaaaaBaa333aBBBBBBJBBJBBBt"}; + +} // namespace + +class FeaturePolicyParserTest : public testing::Test { + protected: + FeaturePolicyParserTest() = default; + + ~FeaturePolicyParserTest() override = default; + + scoped_refptr<const SecurityOrigin> origin_a_ = + SecurityOrigin::CreateFromString(ORIGIN_A); + scoped_refptr<const SecurityOrigin> origin_b_ = + SecurityOrigin::CreateFromString(ORIGIN_B); + scoped_refptr<const SecurityOrigin> origin_c_ = + SecurityOrigin::CreateFromString(ORIGIN_C); + + url::Origin expected_url_origin_a_ = url::Origin::Create(GURL(ORIGIN_A)); + url::Origin expected_url_origin_b_ = url::Origin::Create(GURL(ORIGIN_B)); + url::Origin expected_url_origin_c_ = url::Origin::Create(GURL(ORIGIN_C)); + + const FeatureNameMap test_feature_name_map = { + {"fullscreen", blink::mojom::FeaturePolicyFeature::kFullscreen}, + {"payment", blink::mojom::FeaturePolicyFeature::kPayment}, + {"geolocation", blink::mojom::FeaturePolicyFeature::kGeolocation}}; +}; + +TEST_F(FeaturePolicyParserTest, ParseValidPolicy) { + Vector<String> messages; + for (const char* policy_string : kValidPolicies) { + messages.clear(); + ParseFeaturePolicy(policy_string, origin_a_.get(), origin_b_.get(), + &messages, test_feature_name_map); + EXPECT_EQ(0UL, messages.size()); + } +} + +TEST_F(FeaturePolicyParserTest, ParseInvalidPolicy) { + Vector<String> messages; + for (const char* policy_string : kInvalidPolicies) { + messages.clear(); + ParseFeaturePolicy(policy_string, origin_a_.get(), origin_b_.get(), + &messages, test_feature_name_map); + EXPECT_NE(0UL, messages.size()); + } +} + +TEST_F(FeaturePolicyParserTest, PolicyParsedCorrectly) { + Vector<String> messages; + + // Empty policy. + ParsedFeaturePolicy parsed_policy = ParseFeaturePolicy( + "", origin_a_.get(), origin_b_.get(), &messages, test_feature_name_map); + EXPECT_EQ(0UL, parsed_policy.size()); + + // Simple policy with 'self'. + parsed_policy = + ParseFeaturePolicy("geolocation 'self'", origin_a_.get(), origin_b_.get(), + &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_FALSE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[0].origins.size()); + EXPECT_TRUE( + parsed_policy[0].origins[0].IsSameOriginWith(expected_url_origin_a_)); + + // Simple policy with *. + parsed_policy = + ParseFeaturePolicy("geolocation *", origin_a_.get(), origin_b_.get(), + &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_TRUE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(0UL, parsed_policy[0].origins.size()); + + // Complicated policy. + parsed_policy = ParseFeaturePolicy( + "geolocation *; " + "fullscreen https://example.net https://example.org; " + "payment 'self'", + origin_a_.get(), origin_b_.get(), &messages, test_feature_name_map); + EXPECT_EQ(3UL, parsed_policy.size()); + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_TRUE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(0UL, parsed_policy[0].origins.size()); + EXPECT_EQ(mojom::FeaturePolicyFeature::kFullscreen, parsed_policy[1].feature); + EXPECT_FALSE(parsed_policy[1].matches_all_origins); + EXPECT_FALSE(parsed_policy[1].matches_opaque_src); + EXPECT_EQ(2UL, parsed_policy[1].origins.size()); + EXPECT_TRUE( + parsed_policy[1].origins[0].IsSameOriginWith(expected_url_origin_b_)); + EXPECT_TRUE( + parsed_policy[1].origins[1].IsSameOriginWith(expected_url_origin_c_)); + EXPECT_EQ(mojom::FeaturePolicyFeature::kPayment, parsed_policy[2].feature); + EXPECT_FALSE(parsed_policy[2].matches_all_origins); + EXPECT_FALSE(parsed_policy[2].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[2].origins.size()); + EXPECT_TRUE( + parsed_policy[2].origins[0].IsSameOriginWith(expected_url_origin_a_)); + + // Multiple policies. + parsed_policy = ParseFeaturePolicy( + "geolocation * https://example.net; " + "fullscreen https://example.net none https://example.org," + "payment 'self' badorigin", + origin_a_.get(), origin_b_.get(), &messages, test_feature_name_map); + EXPECT_EQ(3UL, parsed_policy.size()); + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_TRUE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(0UL, parsed_policy[0].origins.size()); + EXPECT_EQ(mojom::FeaturePolicyFeature::kFullscreen, parsed_policy[1].feature); + EXPECT_FALSE(parsed_policy[1].matches_all_origins); + EXPECT_FALSE(parsed_policy[1].matches_opaque_src); + EXPECT_EQ(2UL, parsed_policy[1].origins.size()); + EXPECT_TRUE( + parsed_policy[1].origins[0].IsSameOriginWith(expected_url_origin_b_)); + EXPECT_TRUE( + parsed_policy[1].origins[1].IsSameOriginWith(expected_url_origin_c_)); + EXPECT_EQ(mojom::FeaturePolicyFeature::kPayment, parsed_policy[2].feature); + EXPECT_FALSE(parsed_policy[2].matches_all_origins); + EXPECT_FALSE(parsed_policy[2].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[2].origins.size()); + EXPECT_TRUE( + parsed_policy[2].origins[0].IsSameOriginWith(expected_url_origin_a_)); + + // Header policies with no optional origin lists. + parsed_policy = + ParseFeaturePolicy("geolocation;fullscreen;payment", origin_a_.get(), + nullptr, &messages, test_feature_name_map); + EXPECT_EQ(3UL, parsed_policy.size()); + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_FALSE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[0].origins.size()); + EXPECT_TRUE( + parsed_policy[0].origins[0].IsSameOriginWith(expected_url_origin_a_)); + EXPECT_EQ(mojom::FeaturePolicyFeature::kFullscreen, parsed_policy[1].feature); + EXPECT_FALSE(parsed_policy[1].matches_all_origins); + EXPECT_FALSE(parsed_policy[1].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[1].origins.size()); + EXPECT_TRUE( + parsed_policy[1].origins[0].IsSameOriginWith(expected_url_origin_a_)); + EXPECT_EQ(mojom::FeaturePolicyFeature::kPayment, parsed_policy[2].feature); + EXPECT_FALSE(parsed_policy[2].matches_all_origins); + EXPECT_FALSE(parsed_policy[2].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[2].origins.size()); + EXPECT_TRUE( + parsed_policy[2].origins[0].IsSameOriginWith(expected_url_origin_a_)); +} + +TEST_F(FeaturePolicyParserTest, PolicyParsedCorrectlyForOpaqueOrigins) { + Vector<String> messages; + + scoped_refptr<SecurityOrigin> opaque_origin = + SecurityOrigin::CreateUniqueOpaque(); + + // Empty policy. + ParsedFeaturePolicy parsed_policy = + ParseFeaturePolicy("", origin_a_.get(), opaque_origin.get(), &messages, + test_feature_name_map); + EXPECT_EQ(0UL, parsed_policy.size()); + + // Simple policy. + parsed_policy = + ParseFeaturePolicy("geolocation", origin_a_.get(), opaque_origin.get(), + &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_FALSE(parsed_policy[0].matches_all_origins); + EXPECT_TRUE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(0UL, parsed_policy[0].origins.size()); + + // Simple policy with 'src'. + parsed_policy = + ParseFeaturePolicy("geolocation 'src'", origin_a_.get(), + opaque_origin.get(), &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_FALSE(parsed_policy[0].matches_all_origins); + EXPECT_TRUE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(0UL, parsed_policy[0].origins.size()); + + // Simple policy with *. + parsed_policy = + ParseFeaturePolicy("geolocation *", origin_a_.get(), opaque_origin.get(), + &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_TRUE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(0UL, parsed_policy[0].origins.size()); + + // Policy with explicit origins + parsed_policy = ParseFeaturePolicy( + "geolocation https://example.net https://example.org", origin_a_.get(), + opaque_origin.get(), &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_FALSE(parsed_policy[0].matches_all_origins); + EXPECT_FALSE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(2UL, parsed_policy[0].origins.size()); + EXPECT_TRUE( + parsed_policy[0].origins[0].IsSameOriginWith(expected_url_origin_b_)); + EXPECT_TRUE( + parsed_policy[0].origins[1].IsSameOriginWith(expected_url_origin_c_)); + + // Policy with multiple origins, including 'src'. + parsed_policy = ParseFeaturePolicy("geolocation https://example.net 'src'", + origin_a_.get(), opaque_origin.get(), + &messages, test_feature_name_map); + EXPECT_EQ(1UL, parsed_policy.size()); + + EXPECT_EQ(mojom::FeaturePolicyFeature::kGeolocation, + parsed_policy[0].feature); + EXPECT_FALSE(parsed_policy[0].matches_all_origins); + EXPECT_TRUE(parsed_policy[0].matches_opaque_src); + EXPECT_EQ(1UL, parsed_policy[0].origins.size()); + EXPECT_TRUE( + parsed_policy[0].origins[0].IsSameOriginWith(expected_url_origin_b_)); +} + +// Test histogram counting the use of feature policies in header. +TEST_F(FeaturePolicyParserTest, HeaderHistogram) { + const char* histogram_name = "Blink.UseCounter.FeaturePolicy.Header"; + HistogramTester tester; + Vector<String> messages; + + ParseFeaturePolicy("payment; fullscreen", origin_a_.get(), nullptr, &messages, + test_feature_name_map); + tester.ExpectTotalCount(histogram_name, 2); + tester.ExpectBucketCount( + histogram_name, + static_cast<int>(blink::mojom::FeaturePolicyFeature::kPayment), 1); + tester.ExpectBucketCount( + histogram_name, + static_cast<int>(blink::mojom::FeaturePolicyFeature::kFullscreen), 1); +} + +// Test counting the use of each feature policy only once per header. +TEST_F(FeaturePolicyParserTest, HistogramMultiple) { + const char* histogram_name = "Blink.UseCounter.FeaturePolicy.Header"; + HistogramTester tester; + Vector<String> messages; + + // If the same feature is listed multiple times, it should only be counted + // once. + ParseFeaturePolicy("geolocation 'self'; payment; geolocation *", + origin_a_.get(), nullptr, &messages, + test_feature_name_map); + ParseFeaturePolicy("fullscreen 'self', fullscreen *", origin_a_.get(), + nullptr, &messages, test_feature_name_map); + tester.ExpectTotalCount(histogram_name, 3); + tester.ExpectBucketCount( + histogram_name, + static_cast<int>(blink::mojom::FeaturePolicyFeature::kGeolocation), 1); + tester.ExpectBucketCount( + histogram_name, + static_cast<int>(blink::mojom::FeaturePolicyFeature::kFullscreen), 1); +} + +// Test policy mutation methods +class FeaturePolicyMutationTest : public testing::Test { + protected: + FeaturePolicyMutationTest() = default; + + ~FeaturePolicyMutationTest() override = default; + + url::Origin url_origin_a_ = url::Origin::Create(GURL(ORIGIN_A)); + url::Origin url_origin_b_ = url::Origin::Create(GURL(ORIGIN_B)); + url::Origin url_origin_c_ = url::Origin::Create(GURL(ORIGIN_C)); + + // Returns true if the policy contains a declaration for the feature which + // allows it in all origins. + bool IsFeatureAllowedEverywhere(mojom::FeaturePolicyFeature feature, + const ParsedFeaturePolicy& policy) { + const auto& result = std::find_if(policy.begin(), policy.end(), + [feature](const auto& declaration) { + return declaration.feature == feature; + }); + if (result == policy.end()) + return false; + + return result->feature == feature && result->matches_all_origins && + result->matches_opaque_src && result->origins.empty(); + } + + // Returns true if the policy contains a declaration for the feature which + // disallows it in all origins. + bool IsFeatureDisallowedEverywhere(mojom::FeaturePolicyFeature feature, + const ParsedFeaturePolicy& policy) { + const auto& result = std::find_if(policy.begin(), policy.end(), + [feature](const auto& declaration) { + return declaration.feature == feature; + }); + if (result == policy.end()) + return false; + + return result->feature == feature && !result->matches_all_origins && + !result->matches_opaque_src && result->origins.empty(); + } + + ParsedFeaturePolicy test_policy = {{mojom::FeaturePolicyFeature::kFullscreen, + false, + false, + {url_origin_a_, url_origin_b_}}, + {mojom::FeaturePolicyFeature::kGeolocation, + false, + false, + {url_origin_a_}}}; + ParsedFeaturePolicy empty_policy = {}; +}; +TEST_F(FeaturePolicyMutationTest, TestIsFeatureDeclared) { + EXPECT_TRUE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kFullscreen, test_policy)); + EXPECT_TRUE(IsFeatureDeclared(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kUsb, test_policy)); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kNotFound, test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestIsFeatureDeclaredWithEmptyPolicy) { + EXPECT_FALSE(IsFeatureDeclared(mojom::FeaturePolicyFeature::kFullscreen, + empty_policy)); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kNotFound, empty_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestRemoveAbsentFeature) { + ASSERT_EQ(2UL, test_policy.size()); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kPayment, test_policy)); + EXPECT_FALSE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kPayment, + test_policy)); + ASSERT_EQ(2UL, test_policy.size()); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kPayment, test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestRemoveFromEmptyPolicy) { + ASSERT_EQ(0UL, empty_policy.size()); + EXPECT_FALSE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kPayment, + test_policy)); + ASSERT_EQ(0UL, empty_policy.size()); +} + +TEST_F(FeaturePolicyMutationTest, TestRemoveFeatureIfPresent) { + ASSERT_EQ(2UL, test_policy.size()); + EXPECT_TRUE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kFullscreen, test_policy)); + EXPECT_TRUE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kFullscreen, + test_policy)); + EXPECT_EQ(1UL, test_policy.size()); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kFullscreen, test_policy)); + + // Attempt to remove the feature again + EXPECT_FALSE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kFullscreen, + test_policy)); + EXPECT_EQ(1UL, test_policy.size()); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kFullscreen, test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestRemoveFeatureIfPresentOnSecondFeature) { + ASSERT_EQ(2UL, test_policy.size()); + EXPECT_TRUE(IsFeatureDeclared(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); + EXPECT_TRUE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); + ASSERT_EQ(1UL, test_policy.size()); + EXPECT_FALSE(IsFeatureDeclared(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); + + // Attempt to remove the feature again + EXPECT_FALSE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); + EXPECT_EQ(1UL, test_policy.size()); + EXPECT_FALSE(IsFeatureDeclared(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestRemoveAllFeatures) { + ASSERT_EQ(2UL, test_policy.size()); + EXPECT_TRUE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kFullscreen, + test_policy)); + EXPECT_TRUE(RemoveFeatureIfPresent(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); + EXPECT_EQ(0UL, test_policy.size()); + EXPECT_FALSE( + IsFeatureDeclared(mojom::FeaturePolicyFeature::kFullscreen, test_policy)); + EXPECT_FALSE(IsFeatureDeclared(mojom::FeaturePolicyFeature::kGeolocation, + test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestDisallowIfNotPresent) { + ParsedFeaturePolicy copy = test_policy; + // Try to disallow a feature which already exists + EXPECT_FALSE(DisallowFeatureIfNotPresent( + mojom::FeaturePolicyFeature::kFullscreen, copy)); + ASSERT_EQ(copy, test_policy); + + // Disallow a new feature + EXPECT_TRUE( + DisallowFeatureIfNotPresent(mojom::FeaturePolicyFeature::kPayment, copy)); + EXPECT_EQ(3UL, copy.size()); + // Verify that the feature is, in fact, now disallowed everywhere + EXPECT_TRUE(IsFeatureDisallowedEverywhere( + mojom::FeaturePolicyFeature::kPayment, copy)); +} + +TEST_F(FeaturePolicyMutationTest, TestAllowEverywhereIfNotPresent) { + ParsedFeaturePolicy copy = test_policy; + // Try to allow a feature which already exists + EXPECT_FALSE(AllowFeatureEverywhereIfNotPresent( + mojom::FeaturePolicyFeature::kFullscreen, copy)); + ASSERT_EQ(copy, test_policy); + + // Allow a new feature + EXPECT_TRUE(AllowFeatureEverywhereIfNotPresent( + mojom::FeaturePolicyFeature::kPayment, copy)); + EXPECT_EQ(3UL, copy.size()); + // Verify that the feature is, in fact, allowed everywhere + EXPECT_TRUE( + IsFeatureAllowedEverywhere(mojom::FeaturePolicyFeature::kPayment, copy)); +} + +TEST_F(FeaturePolicyMutationTest, TestDisallowUnconditionally) { + // Try to disallow a feature which already exists + DisallowFeature(mojom::FeaturePolicyFeature::kFullscreen, test_policy); + // Should not have changed the number of declarations + EXPECT_EQ(2UL, test_policy.size()); + // Verify that the feature is, in fact, now disallowed everywhere + EXPECT_TRUE(IsFeatureDisallowedEverywhere( + mojom::FeaturePolicyFeature::kFullscreen, test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestDisallowNewFeatureUnconditionally) { + // Try to disallow a feature which does not yet exist + DisallowFeature(mojom::FeaturePolicyFeature::kPayment, test_policy); + // Should have added a new declaration + EXPECT_EQ(3UL, test_policy.size()); + // Verify that the feature is, in fact, now disallowed everywhere + EXPECT_TRUE(IsFeatureDisallowedEverywhere( + mojom::FeaturePolicyFeature::kPayment, test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestAllowUnconditionally) { + // Try to allow a feature which already exists + AllowFeatureEverywhere(mojom::FeaturePolicyFeature::kFullscreen, test_policy); + // Should not have changed the number of declarations + EXPECT_EQ(2UL, test_policy.size()); + // Verify that the feature is, in fact, now allowed everywhere + EXPECT_TRUE(IsFeatureAllowedEverywhere( + mojom::FeaturePolicyFeature::kFullscreen, test_policy)); +} + +TEST_F(FeaturePolicyMutationTest, TestAllowNewFeatureUnconditionally) { + // Try to allow a feature which does not yet exist + AllowFeatureEverywhere(mojom::FeaturePolicyFeature::kPayment, test_policy); + // Should have added a new declaration + EXPECT_EQ(3UL, test_policy.size()); + // Verify that the feature is, in fact, now allowed everywhere + EXPECT_TRUE(IsFeatureAllowedEverywhere(mojom::FeaturePolicyFeature::kPayment, + test_policy)); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/feature_policy/iframe_policy.h b/chromium/third_party/blink/renderer/core/feature_policy/iframe_policy.h new file mode 100644 index 00000000000..e53ec6427e9 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/iframe_policy.h @@ -0,0 +1,56 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_IFRAME_POLICY_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_IFRAME_POLICY_H_ + +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/feature_policy/policy.h" +#include "third_party/blink/renderer/platform/heap/member.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" + +namespace blink { + +// IFramePolicy inherits Policy. It represents the feature policy introspection +// of an iframe contained in a document. It is tynthetic from the parent policy +// and the iframe container policy (parsed from the allow attribute). +class IFramePolicy final : public Policy { + public: + ~IFramePolicy() override = default; + + // Create a new IFramePolicy, which is synthetic for a frame contained within + // a document. + IFramePolicy(Document* parent_document, + const ParsedFeaturePolicy& container_policy, + scoped_refptr<const SecurityOrigin> src_origin) + : parent_document_(parent_document) { + DCHECK(src_origin); + UpdateContainerPolicy(container_policy, src_origin); + } + + void UpdateContainerPolicy( + const ParsedFeaturePolicy& container_policy, + scoped_refptr<const SecurityOrigin> src_origin) override { + policy_ = FeaturePolicy::CreateFromParentPolicy( + parent_document_->GetFeaturePolicy(), container_policy, + src_origin->ToUrlOrigin()); + } + + void Trace(blink::Visitor* visitor) override { + visitor->Trace(parent_document_); + Policy::Trace(visitor); + } + + protected: + const FeaturePolicy* GetPolicy() const override { return policy_.get(); } + Document* GetDocument() const override { return parent_document_; } + + private: + Member<Document> parent_document_; + std::unique_ptr<FeaturePolicy> policy_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_IFRAME_POLICY_H_ diff --git a/chromium/third_party/blink/renderer/core/feature_policy/policy.cc b/chromium/third_party/blink/renderer/core/feature_policy/policy.cc new file mode 100644 index 00000000000..474a4771017 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/policy.cc @@ -0,0 +1,85 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/feature_policy/policy.h" + +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/feature_policy/feature_policy.h" +#include "third_party/blink/renderer/core/inspector/console_message.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" + +namespace blink { + +bool Policy::allowsFeature(const String& feature) const { + if (GetDefaultFeatureNameMap().Contains(feature)) { + return GetPolicy()->IsFeatureEnabled( + GetDefaultFeatureNameMap().at(feature)); + } + + AddWarningForUnrecognizedFeature(feature); + return false; +} + +bool Policy::allowsFeature(const String& feature, const String& url) const { + scoped_refptr<const SecurityOrigin> origin = + SecurityOrigin::CreateFromString(url); + if (!origin || origin->IsOpaque()) { + GetDocument()->AddConsoleMessage(ConsoleMessage::Create( + kOtherMessageSource, kWarningMessageLevel, + "Invalid origin url for feature '" + feature + "': " + url + ".")); + return false; + } + + if (!GetDefaultFeatureNameMap().Contains(feature)) { + AddWarningForUnrecognizedFeature(feature); + return false; + } + + return GetPolicy()->IsFeatureEnabledForOrigin( + GetDefaultFeatureNameMap().at(feature), origin->ToUrlOrigin()); +} + +Vector<String> Policy::allowedFeatures() const { + Vector<String> allowed_features; + for (const auto& entry : GetDefaultFeatureNameMap()) { + if (GetPolicy()->IsFeatureEnabled(entry.value)) + allowed_features.push_back(entry.key); + } + return allowed_features; +} + +Vector<String> Policy::getAllowlistForFeature(const String& feature) const { + if (GetDefaultFeatureNameMap().Contains(feature)) { + const FeaturePolicy::Allowlist allowlist = + GetPolicy()->GetAllowlistForFeature( + GetDefaultFeatureNameMap().at(feature)); + if (allowlist.MatchesAll()) + return Vector<String>({"*"}); + Vector<String> result; + for (const auto& origin : allowlist.Origins()) { + result.push_back(WTF::String::FromUTF8(origin.Serialize().c_str())); + } + return result; + } + + AddWarningForUnrecognizedFeature(feature); + return Vector<String>(); +} + +void Policy::AddWarningForUnrecognizedFeature(const String& feature) const { + GetDocument()->AddConsoleMessage( + ConsoleMessage::Create(kOtherMessageSource, kWarningMessageLevel, + "Unrecognized feature: '" + feature + "'.")); +} + +void Policy::Trace(blink::Visitor* visitor) { + ScriptWrappable::Trace(visitor); +} + +void Policy::UpdateContainerPolicy( + const ParsedFeaturePolicy& container_policy, + scoped_refptr<const SecurityOrigin> src_origin) {} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/feature_policy/policy.h b/chromium/third_party/blink/renderer/core/feature_policy/policy.h new file mode 100644 index 00000000000..1f066f24c00 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/policy.h @@ -0,0 +1,59 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_POLICY_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_POLICY_H_ + +#include "third_party/blink/public/common/feature_policy/feature_policy.h" +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/platform/bindings/script_wrappable.h" +#include "third_party/blink/renderer/platform/heap/member.h" + +namespace blink { + +class Document; +class SecurityOrigin; + +// Policy provides an interface for feature policy introspection of a document +// (DocumentPolicy) or an iframe (IFramePolicy). +class CORE_EXPORT Policy : public ScriptWrappable { + DEFINE_WRAPPERTYPEINFO(); + + public: + ~Policy() override = default; + + // Implementation of methods of the policy interface: + // Returns whether or not the given feature is allowed on the origin of the + // document that owns the policy. + bool allowsFeature(const String& feature) const; + // Returns whether or not the given feature is allowed on the origin of the + // given URL. + bool allowsFeature(const String& feature, const String& url) const; + // Returns a list of feature names that are allowed on the self origin. + Vector<String> allowedFeatures() const; + // Returns a list of feature name that are allowed on the origin of the given + // URL. + Vector<String> getAllowlistForFeature(const String& url) const; + + // Inform the Policy object when the container policy on its frame element has + // changed. + virtual void UpdateContainerPolicy( + const ParsedFeaturePolicy& container_policy = {}, + scoped_refptr<const SecurityOrigin> src_origin = nullptr); + + void Trace(blink::Visitor*) override; + + protected: + virtual const FeaturePolicy* GetPolicy() const = 0; + // Get the containing document. + virtual Document* GetDocument() const = 0; + + private: + // Add console message to the containing document. + void AddWarningForUnrecognizedFeature(const String& message) const; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_FEATURE_POLICY_POLICY_H_ diff --git a/chromium/third_party/blink/renderer/core/feature_policy/policy.idl b/chromium/third_party/blink/renderer/core/feature_policy/policy.idl new file mode 100644 index 00000000000..d2d5ad3169d --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/policy.idl @@ -0,0 +1,16 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// https://wicg.github.io/feature-policy +// TODO(iclelland): add spec for JS exposure in the spec for Feature Policy. +// Please refer to this doc for more details for now: +// https://docs.google.com/a/chromium.org/document/d/1wvk3cXkblNnbkMcsKayseK-k0SMGiP9b9fQFgfpqQpc/edit?usp=sharing +[ + NoInterfaceObject, + OriginTrialEnabled=FeaturePolicyJavaScriptInterface +] interface Policy { + [MeasureAs=FeaturePolicyJSAPI] boolean allowsFeature(DOMString feature, optional DOMString url); + [MeasureAs=FeaturePolicyJSAPI] sequence<DOMString> allowedFeatures(); + [MeasureAs=FeaturePolicyJSAPI] sequence<DOMString> getAllowlistForFeature(DOMString feature); +}; diff --git a/chromium/third_party/blink/renderer/core/feature_policy/policy_test.cc b/chromium/third_party/blink/renderer/core/feature_policy/policy_test.cc new file mode 100644 index 00000000000..93d689f3e1e --- /dev/null +++ b/chromium/third_party/blink/renderer/core/feature_policy/policy_test.cc @@ -0,0 +1,170 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/feature_policy/document_policy.h" +#include "third_party/blink/renderer/core/feature_policy/iframe_policy.h" + +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/feature_policy/feature_policy.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" + +namespace blink { + +namespace { +constexpr char kSelfOrigin[] = "https://selforigin.com"; +constexpr char kOriginA[] = "https://example.com"; +constexpr char kOriginB[] = "https://example.net"; +} // namespace + +using testing::UnorderedElementsAre; + +class PolicyTest : public testing::Test { + public: + void SetUp() override { + document_ = Document::CreateForTest(); + document_->SetSecurityOrigin(SecurityOrigin::CreateFromString(kSelfOrigin)); + document_->ApplyFeaturePolicyFromHeader( + "fullscreen *; payment 'self'; midi 'none'; camera 'self' " + "https://example.com https://example.net"); + } + + Policy* GetPolicy() const { return policy_; } + + protected: + Persistent<Document> document_; + Persistent<Policy> policy_; +}; + +class DocumentPolicyTest : public PolicyTest { + public: + void SetUp() override { + PolicyTest::SetUp(); + policy_ = new DocumentPolicy(document_); + } +}; + +class IFramePolicyTest : public PolicyTest { + public: + void SetUp() override { + PolicyTest::SetUp(); + policy_ = new IFramePolicy(document_, {}, + SecurityOrigin::CreateFromString(kSelfOrigin)); + } +}; + +TEST_F(DocumentPolicyTest, TestAllowsFeature) { + EXPECT_FALSE(GetPolicy()->allowsFeature("badfeature")); + EXPECT_FALSE(GetPolicy()->allowsFeature("midi")); + EXPECT_FALSE(GetPolicy()->allowsFeature("midi", kSelfOrigin)); + EXPECT_TRUE(GetPolicy()->allowsFeature("fullscreen")); + EXPECT_TRUE(GetPolicy()->allowsFeature("fullscreen", kOriginA)); + EXPECT_TRUE(GetPolicy()->allowsFeature("payment")); + EXPECT_FALSE(GetPolicy()->allowsFeature("payment", kOriginA)); + EXPECT_FALSE(GetPolicy()->allowsFeature("payment", kOriginB)); + EXPECT_TRUE(GetPolicy()->allowsFeature("camera")); + EXPECT_TRUE(GetPolicy()->allowsFeature("camera", kOriginA)); + EXPECT_TRUE(GetPolicy()->allowsFeature("camera", kOriginB)); + EXPECT_FALSE(GetPolicy()->allowsFeature("camera", "https://badorigin.com")); + EXPECT_TRUE(GetPolicy()->allowsFeature("geolocation", kSelfOrigin)); + EXPECT_TRUE(GetPolicy()->allowsFeature("sync-xhr")); + EXPECT_TRUE(GetPolicy()->allowsFeature("sync-xhr", kOriginA)); +} + +TEST_F(DocumentPolicyTest, TestGetAllowList) { + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("camera"), + UnorderedElementsAre(kSelfOrigin, kOriginA, kOriginB)); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("payment"), + UnorderedElementsAre(kSelfOrigin)); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("geolocation"), + UnorderedElementsAre(kSelfOrigin)); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("fullscreen"), + UnorderedElementsAre("*")); + EXPECT_TRUE(GetPolicy()->getAllowlistForFeature("badfeature").IsEmpty()); + EXPECT_TRUE(GetPolicy()->getAllowlistForFeature("midi").IsEmpty()); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("sync-xhr"), + UnorderedElementsAre("*")); +} + +TEST_F(DocumentPolicyTest, TestAllowedFeatures) { + Vector<String> allowed_features = GetPolicy()->allowedFeatures(); + EXPECT_TRUE(allowed_features.Contains("fullscreen")); + EXPECT_TRUE(allowed_features.Contains("payment")); + EXPECT_TRUE(allowed_features.Contains("camera")); + // "geolocation" has default policy as allowed on self origin. + EXPECT_TRUE(allowed_features.Contains("geolocation")); + EXPECT_FALSE(allowed_features.Contains("badfeature")); + EXPECT_FALSE(allowed_features.Contains("midi")); + // "sync-xhr" is allowed on all origins + EXPECT_TRUE(allowed_features.Contains("sync-xhr")); +} + +TEST_F(IFramePolicyTest, TestAllowsFeature) { + EXPECT_FALSE(GetPolicy()->allowsFeature("badfeature")); + EXPECT_FALSE(GetPolicy()->allowsFeature("midi")); + EXPECT_FALSE(GetPolicy()->allowsFeature("midi", kSelfOrigin)); + EXPECT_TRUE(GetPolicy()->allowsFeature("fullscreen")); + EXPECT_FALSE(GetPolicy()->allowsFeature("fullscreen", kOriginA)); + EXPECT_TRUE(GetPolicy()->allowsFeature("fullscreen", kSelfOrigin)); + EXPECT_TRUE(GetPolicy()->allowsFeature("payment")); + EXPECT_FALSE(GetPolicy()->allowsFeature("payment", kOriginA)); + EXPECT_FALSE(GetPolicy()->allowsFeature("payment", kOriginB)); + EXPECT_TRUE(GetPolicy()->allowsFeature("camera")); + EXPECT_FALSE(GetPolicy()->allowsFeature("camera", kOriginA)); + EXPECT_FALSE(GetPolicy()->allowsFeature("camera", kOriginB)); + EXPECT_FALSE(GetPolicy()->allowsFeature("camera", "https://badorigin.com")); + EXPECT_TRUE(GetPolicy()->allowsFeature("geolocation", kSelfOrigin)); + EXPECT_TRUE(GetPolicy()->allowsFeature("sync-xhr")); + EXPECT_TRUE(GetPolicy()->allowsFeature("sync-xhr", kOriginA)); +} + +TEST_F(IFramePolicyTest, TestGetAllowList) { + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("camera"), + UnorderedElementsAre(kSelfOrigin)); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("payment"), + UnorderedElementsAre(kSelfOrigin)); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("geolocation"), + UnorderedElementsAre(kSelfOrigin)); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("fullscreen"), + UnorderedElementsAre(kSelfOrigin)); + EXPECT_TRUE(GetPolicy()->getAllowlistForFeature("badfeature").IsEmpty()); + EXPECT_TRUE(GetPolicy()->getAllowlistForFeature("midi").IsEmpty()); + EXPECT_THAT(GetPolicy()->getAllowlistForFeature("sync-xhr"), + UnorderedElementsAre("*")); +} + +TEST_F(IFramePolicyTest, TestAllowedFeatures) { + Vector<String> allowed_features = GetPolicy()->allowedFeatures(); + EXPECT_TRUE(allowed_features.Contains("fullscreen")); + EXPECT_TRUE(allowed_features.Contains("payment")); + EXPECT_TRUE(allowed_features.Contains("camera")); + // "geolocation" has default policy as allowed on self origin. + EXPECT_TRUE(allowed_features.Contains("geolocation")); + EXPECT_FALSE(allowed_features.Contains("badfeature")); + EXPECT_FALSE(allowed_features.Contains("midi")); + // "sync-xhr" is allowed on all origins + EXPECT_TRUE(allowed_features.Contains("sync-xhr")); +} + +TEST_F(IFramePolicyTest, TestCombinedPolicy) { + ParsedFeaturePolicy container_policy = ParseFeaturePolicyAttribute( + "geolocation 'src'; payment 'none'; midi; camera 'src'", + SecurityOrigin::CreateFromString(kSelfOrigin), + SecurityOrigin::CreateFromString(kOriginA), nullptr); + GetPolicy()->UpdateContainerPolicy( + container_policy, SecurityOrigin::CreateFromString(kOriginA)); + Vector<String> allowed_features = GetPolicy()->allowedFeatures(); + EXPECT_TRUE(allowed_features.Contains("fullscreen")); + EXPECT_FALSE(allowed_features.Contains("payment")); + EXPECT_TRUE(allowed_features.Contains("geolocation")); + EXPECT_FALSE(allowed_features.Contains("midi")); + EXPECT_TRUE(allowed_features.Contains("camera")); + // "geolocation" has default policy as allowed on self origin. + EXPECT_FALSE(allowed_features.Contains("badfeature")); + // "sync-xhr" is still implicitly allowed on all origins + EXPECT_TRUE(allowed_features.Contains("sync-xhr")); +} + +} // namespace blink |