| // 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/permissions_policy/permissions_policy_parser.h" |
| |
| #include <algorithm> |
| #include <bitset> |
| #include <utility> |
| |
| #include "base/compiler_specific.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "net/http/structured_headers.h" |
| #include "services/network/public/cpp/permissions_policy/origin_with_possible_wildcards.h" |
| #include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/web/web_navigation_params.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/web_feature.h" |
| #include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h" |
| #include "third_party/blink/renderer/core/origin_trials/origin_trial_context.h" |
| #include "third_party/blink/renderer/platform/allow_discouraged_type.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| #include "third_party/blink/renderer/platform/json/json_values.h" |
| #include "third_party/blink/renderer/platform/network/http_parsers.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_origin.h" |
| #include "third_party/blink/renderer/platform/wtf/casting.h" |
| #include "third_party/blink/renderer/platform/wtf/hash_set.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" |
| #include "url/origin.h" |
| |
| namespace blink { |
| namespace { |
| |
| class ParsedFeaturePolicies final |
| : public GarbageCollected<ParsedFeaturePolicies>, |
| public Supplement<ExecutionContext> { |
| public: |
| static const char kSupplementName[]; |
| |
| static ParsedFeaturePolicies& From(ExecutionContext& context) { |
| ParsedFeaturePolicies* policies = |
| Supplement<ExecutionContext>::From<ParsedFeaturePolicies>(context); |
| if (!policies) { |
| policies = MakeGarbageCollected<ParsedFeaturePolicies>(context); |
| Supplement<ExecutionContext>::ProvideTo(context, policies); |
| } |
| return *policies; |
| } |
| |
| explicit ParsedFeaturePolicies(ExecutionContext& context) |
| : Supplement<ExecutionContext>(context), |
| policies_(static_cast<size_t>( |
| network::mojom::PermissionsPolicyFeature::kMaxValue) + |
| 1) {} |
| |
| bool Observed(network::mojom::PermissionsPolicyFeature feature) { |
| wtf_size_t feature_index = static_cast<wtf_size_t>(feature); |
| if (policies_[feature_index]) { |
| return true; |
| } |
| policies_[feature_index] = true; |
| return false; |
| } |
| |
| private: |
| // Tracks which permissions policies have already been parsed, so as not to |
| // count them multiple times. |
| Vector<bool> policies_; |
| }; |
| |
| const char ParsedFeaturePolicies::kSupplementName[] = "ParsedFeaturePolicies"; |
| |
| class FeatureObserver { |
| public: |
| // Returns whether the feature has been observed before or not. |
| bool FeatureObserved(network::mojom::PermissionsPolicyFeature feature); |
| |
| private: |
| std::bitset<static_cast<size_t>( |
| network::mojom::PermissionsPolicyFeature::kMaxValue) + |
| 1> |
| features_specified_; |
| }; |
| |
| class ParsingContext { |
| STACK_ALLOCATED(); |
| |
| public: |
| ParsingContext(PolicyParserMessageBuffer& logger, |
| const SecurityOrigin& self_origin LIFETIME_CAPTURE_BY_THIS, |
| const SecurityOrigin* src_origin LIFETIME_CAPTURE_BY_THIS, |
| const FeatureNameMap& feature_names, |
| ExecutionContext* execution_context) |
| : logger_(logger), |
| self_origin_(self_origin), |
| src_origin_(src_origin), |
| feature_names_(feature_names), |
| execution_context_(execution_context) {} |
| |
| ~ParsingContext() = default; |
| |
| network::ParsedPermissionsPolicy ParseFeaturePolicy(const String& policy); |
| network::ParsedPermissionsPolicy ParsePermissionsPolicy(const String& policy); |
| network::ParsedPermissionsPolicy ParsePolicyFromNode( |
| const PermissionsPolicyParser::Node& root); |
| |
| private: |
| PermissionsPolicyParser::Node ParseFeaturePolicyToIR(const String& policy); |
| PermissionsPolicyParser::Node ParsePermissionsPolicyToIR( |
| const String& policy); |
| |
| // normally 1 char = 1 byte |
| // max length to parse = 2^16 = 64 kB |
| static constexpr wtf_size_t MAX_LENGTH_PARSE = 1 << 16; |
| |
| std::optional<network::ParsedPermissionsPolicyDeclaration> ParseFeature( |
| const PermissionsPolicyParser::Declaration& declaration_node, |
| const network::OriginWithPossibleWildcards::NodeType type); |
| |
| struct ParsedAllowlist { |
| std::vector<network::OriginWithPossibleWildcards> allowed_origins |
| ALLOW_DISCOURAGED_TYPE("Permission policy uses STL for code sharing"); |
| std::optional<url::Origin> self_if_matches; |
| bool matches_all_origins{false}; |
| bool matches_opaque_src{false}; |
| |
| ParsedAllowlist() : allowed_origins({}) {} |
| }; |
| |
| std::optional<network::mojom::PermissionsPolicyFeature> ParseFeatureName( |
| const String& feature_name); |
| |
| // Parse allowlist for feature. |
| ParsedAllowlist ParseAllowlist( |
| const Vector<String>& origin_strings, |
| const network::OriginWithPossibleWildcards::NodeType type); |
| |
| void ReportFeatureUsage(network::mojom::PermissionsPolicyFeature feature); |
| void ReportFeatureUsageLegacy( |
| network::mojom::PermissionsPolicyFeature feature); |
| |
| // This function should be called after Allowlist Histograms related flags |
| // have been captured. |
| void RecordAllowlistTypeUsage(size_t origin_count); |
| |
| PolicyParserMessageBuffer& logger_; |
| const SecurityOrigin& self_origin_; |
| const SecurityOrigin* const src_origin_; |
| const FeatureNameMap& feature_names_; |
| // `execution_context_` is used for reporting various WebFeatures |
| // during the parsing process. |
| // `execution_context_` should only be `nullptr` in tests. |
| ExecutionContext* execution_context_; |
| |
| FeatureObserver feature_observer_; |
| }; |
| |
| bool FeatureObserver::FeatureObserved( |
| network::mojom::PermissionsPolicyFeature feature) { |
| if (features_specified_[static_cast<size_t>(feature)]) { |
| return true; |
| } else { |
| features_specified_.set(static_cast<size_t>(feature)); |
| return false; |
| } |
| } |
| |
| // TODO: Remove this function once we verified the new histogram counts |
| // are consistent with old ones. |
| void ParsingContext::ReportFeatureUsageLegacy( |
| network::mojom::PermissionsPolicyFeature feature) { |
| if (!src_origin_) { |
| UMA_HISTOGRAM_ENUMERATION("Blink.UseCounter.FeaturePolicy.Header", feature); |
| } |
| } |
| |
| void ParsingContext::ReportFeatureUsage( |
| network::mojom::PermissionsPolicyFeature feature) { |
| if (!execution_context_ || !execution_context_->IsWindow()) { |
| return; |
| } |
| |
| LocalDOMWindow* local_dom_window = To<LocalDOMWindow>(execution_context_); |
| |
| auto usage_type = |
| src_origin_ ? UseCounterImpl::PermissionsPolicyUsageType::kIframeAttribute |
| : UseCounterImpl::PermissionsPolicyUsageType::kHeader; |
| |
| local_dom_window->CountPermissionsPolicyUsage(feature, usage_type); |
| } |
| |
| std::optional<network::mojom::PermissionsPolicyFeature> |
| ParsingContext::ParseFeatureName(const String& feature_name) { |
| DCHECK(!feature_name.empty()); |
| if (!feature_names_.Contains(feature_name)) { |
| logger_.Warn(StrCat({"Unrecognized feature: '", feature_name, "'."})); |
| return std::nullopt; |
| } |
| if (DisabledByOriginTrial(feature_name, execution_context_)) { |
| logger_.Warn(StrCat({"Origin trial controlled feature not enabled: '", |
| feature_name, "'."})); |
| return std::nullopt; |
| } |
| network::mojom::PermissionsPolicyFeature feature = |
| feature_names_.at(feature_name); |
| |
| if (feature == network::mojom::PermissionsPolicyFeature::kUnload) { |
| UseCounter::Count(execution_context_, WebFeature::kPermissionsPolicyUnload); |
| } |
| return feature; |
| } |
| |
| ParsingContext::ParsedAllowlist ParsingContext::ParseAllowlist( |
| const Vector<String>& origin_strings, |
| const network::OriginWithPossibleWildcards::NodeType type) { |
| // The source of the PermissionsPolicyParser::Node must have an explicit |
| // source so that we know which wildcards can be enabled. |
| DCHECK_NE(network::OriginWithPossibleWildcards::NodeType::kUnknown, type); |
| ParsedAllowlist allowlist; |
| if (origin_strings.empty()) { |
| // If a policy entry has no listed origins (e.g. "feature_name1" in |
| // 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 (!src_origin_) { |
| allowlist.self_if_matches = self_origin_.ToUrlOrigin(); |
| } else if (!src_origin_->IsOpaque()) { |
| std::optional<network::OriginWithPossibleWildcards> |
| maybe_origin_with_possible_wildcards = |
| network::OriginWithPossibleWildcards::FromOrigin( |
| src_origin_->ToUrlOrigin()); |
| if (maybe_origin_with_possible_wildcards.has_value()) { |
| allowlist.allowed_origins.emplace_back( |
| *maybe_origin_with_possible_wildcards); |
| } |
| } else { |
| allowlist.matches_opaque_src = true; |
| } |
| } else { |
| for (const String& origin_string : origin_strings) { |
| DCHECK(!origin_string.empty()); |
| |
| if (!origin_string.ContainsOnlyAsciiOrEmpty()) { |
| logger_.Warn("Non-ASCII characters in origin."); |
| continue; |
| } |
| |
| // Determine the target of the declaration. This may be a specific |
| // origin, either explicitly written, or one of the special keywords |
| // 'self' or 'src'. ('src' can only be used in the iframe allow |
| // attribute.) Also determine if this target has a subdomain wildcard |
| // (e.g., https://*.google.com). |
| network::OriginWithPossibleWildcards origin_with_possible_wildcards; |
| |
| // 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. |
| bool target_is_opaque = false; |
| bool target_is_all = false; |
| bool target_is_self = false; |
| url::Origin self; |
| |
| // 'self' origin is used if the origin is exactly 'self'. |
| if (EqualIgnoringAsciiCase(origin_string, "'self'")) { |
| target_is_self = true; |
| self = self_origin_.ToUrlOrigin(); |
| } |
| // 'src' origin is used if |src_origin| is available and the |
| // origin is a match for 'src'. |src_origin| is only set |
| // when parsing an iframe allow attribute. |
| else if (src_origin_ && EqualIgnoringAsciiCase(origin_string, "'src'")) { |
| if (!src_origin_->IsOpaque()) { |
| std::optional<network::OriginWithPossibleWildcards> |
| maybe_origin_with_possible_wildcards = |
| network::OriginWithPossibleWildcards::FromOrigin( |
| src_origin_->ToUrlOrigin()); |
| if (maybe_origin_with_possible_wildcards.has_value()) { |
| origin_with_possible_wildcards = |
| *maybe_origin_with_possible_wildcards; |
| } else { |
| continue; |
| } |
| } else { |
| target_is_opaque = true; |
| } |
| } else if (EqualIgnoringAsciiCase(origin_string, "'none'")) { |
| continue; |
| } else if (origin_string == "*") { |
| target_is_all = true; |
| } |
| // Otherwise, parse the origin string and verify that the result is |
| // valid. Invalid strings will produce an opaque origin, which will |
| // result in an error message. |
| else { |
| std::optional<network::OriginWithPossibleWildcards> |
| maybe_origin_with_possible_wildcards = |
| network::OriginWithPossibleWildcards::Parse( |
| origin_string.Utf8(), type); |
| if (maybe_origin_with_possible_wildcards.has_value()) { |
| origin_with_possible_wildcards = |
| *maybe_origin_with_possible_wildcards; |
| } else { |
| logger_.Warn(StrCat({"Unrecognized origin: '", origin_string, "'."})); |
| continue; |
| } |
| } |
| |
| if (target_is_all) { |
| allowlist.matches_all_origins = true; |
| allowlist.matches_opaque_src = true; |
| } else if (target_is_opaque) { |
| allowlist.matches_opaque_src = true; |
| } else if (target_is_self) { |
| allowlist.self_if_matches = self; |
| } else { |
| allowlist.allowed_origins.emplace_back(origin_with_possible_wildcards); |
| } |
| } |
| } |
| |
| // Size reduction: remove all items in the allowlist if target is all. |
| if (allowlist.matches_all_origins) { |
| allowlist.allowed_origins.clear(); |
| } |
| |
| // Sort |allowed_origins| in alphabetical order. |
| std::sort(allowlist.allowed_origins.begin(), allowlist.allowed_origins.end()); |
| |
| return allowlist; |
| } |
| |
| std::optional<network::ParsedPermissionsPolicyDeclaration> |
| ParsingContext::ParseFeature( |
| const PermissionsPolicyParser::Declaration& declaration_node, |
| const network::OriginWithPossibleWildcards::NodeType type) { |
| std::optional<network::mojom::PermissionsPolicyFeature> feature = |
| ParseFeatureName(declaration_node.feature_name); |
| if (!feature) { |
| return std::nullopt; |
| } |
| |
| ParsedAllowlist parsed_allowlist = |
| ParseAllowlist(declaration_node.allowlist, type); |
| |
| // If same feature appeared more than once, only the first one counts. |
| if (feature_observer_.FeatureObserved(*feature)) { |
| return std::nullopt; |
| } |
| |
| network::ParsedPermissionsPolicyDeclaration parsed_feature(*feature); |
| parsed_feature.allowed_origins = std::move(parsed_allowlist.allowed_origins); |
| parsed_feature.self_if_matches = parsed_allowlist.self_if_matches; |
| parsed_feature.matches_all_origins = parsed_allowlist.matches_all_origins; |
| parsed_feature.matches_opaque_src = parsed_allowlist.matches_opaque_src; |
| if (declaration_node.endpoint.IsNull()) { |
| parsed_feature.reporting_endpoint = std::nullopt; |
| } else { |
| parsed_feature.reporting_endpoint = declaration_node.endpoint.Ascii(); |
| } |
| |
| return parsed_feature; |
| } |
| |
| network::ParsedPermissionsPolicy ParsingContext::ParseFeaturePolicy( |
| const String& policy) { |
| return ParsePolicyFromNode(ParseFeaturePolicyToIR(policy)); |
| } |
| |
| network::ParsedPermissionsPolicy ParsingContext::ParsePermissionsPolicy( |
| const String& policy) { |
| return ParsePolicyFromNode(ParsePermissionsPolicyToIR(policy)); |
| } |
| |
| network::ParsedPermissionsPolicy ParsingContext::ParsePolicyFromNode( |
| const PermissionsPolicyParser::Node& root) { |
| network::ParsedPermissionsPolicy parsed_policy; |
| for (const PermissionsPolicyParser::Declaration& declaration_node : |
| root.declarations) { |
| std::optional<network::ParsedPermissionsPolicyDeclaration> parsed_feature = |
| ParseFeature(declaration_node, root.type); |
| if (parsed_feature) { |
| ReportFeatureUsage(parsed_feature->feature); |
| ReportFeatureUsageLegacy(parsed_feature->feature); |
| parsed_policy.push_back(*parsed_feature); |
| } |
| } |
| return parsed_policy; |
| } |
| |
| PermissionsPolicyParser::Node ParsingContext::ParseFeaturePolicyToIR( |
| const String& policy) { |
| PermissionsPolicyParser::Node root{ |
| network::OriginWithPossibleWildcards::NodeType::kAttribute}; |
| |
| if (policy.length() > MAX_LENGTH_PARSE) { |
| logger_.Error(StrCat({"Feature policy declaration exceeds size limit(", |
| String::Number(policy.length()), ">", |
| String::Number(MAX_LENGTH_PARSE), ")"})); |
| return {}; |
| } |
| |
| Vector<String> policy_items; |
| |
| if (src_origin_) { |
| // Attribute parsing. |
| policy_items.push_back(policy); |
| } else { |
| // Header parsing. |
| // 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. |
| // policy_items = [ policy *( "," [ policy ] ) ] |
| policy_items = policy.SplitSkippingEmpty(','); |
| } |
| |
| if (policy_items.size() > 1) { |
| UseCounter::Count( |
| execution_context_, |
| mojom::blink::WebFeature::kFeaturePolicyCommaSeparatedDeclarations); |
| } |
| |
| for (const String& item : policy_items) { |
| // feature_entries = [ feature_entry *( ";" [ feature_entry ] ) ] |
| Vector<String> feature_entries = item.SplitSkippingEmpty(';'); |
| |
| if (feature_entries.size() > 1) { |
| UseCounter::Count(execution_context_, |
| mojom::blink::WebFeature:: |
| kFeaturePolicySemicolonSeparatedDeclarations); |
| } |
| |
| for (const String& feature_entry : feature_entries) { |
| Vector<String> tokens = SplitOnASCIIWhitespace(feature_entry); |
| |
| if (tokens.empty()) { |
| continue; |
| } |
| |
| PermissionsPolicyParser::Declaration declaration_node; |
| // Break tokens into head & tail, where |
| // head = feature_name |
| // tail = allowlist |
| // After feature_name has been set, take tail of tokens vector by |
| // erasing the first element. |
| declaration_node.feature_name = std::move(tokens.front()); |
| tokens.erase(tokens.begin()); |
| declaration_node.allowlist = std::move(tokens); |
| root.declarations.push_back(declaration_node); |
| } |
| } |
| |
| return root; |
| } |
| |
| PermissionsPolicyParser::Node ParsingContext::ParsePermissionsPolicyToIR( |
| const String& policy) { |
| if (policy.length() > MAX_LENGTH_PARSE) { |
| logger_.Error(StrCat({"Permissions policy declaration exceeds size limit(", |
| String::Number(policy.length()), ">", |
| String::Number(MAX_LENGTH_PARSE), ")"})); |
| return {}; |
| } |
| |
| auto root = net::structured_headers::ParseDictionary(policy.Utf8()); |
| if (!root) { |
| logger_.Error( |
| "Parse of permissions policy failed because of errors reported by " |
| "structured header parser."); |
| return {}; |
| } |
| |
| PermissionsPolicyParser::Node ir_root{ |
| network::OriginWithPossibleWildcards::NodeType::kHeader}; |
| for (const auto& feature_entry : root.value()) { |
| const auto& key = feature_entry.first; |
| const char* feature_name = key.c_str(); |
| const auto& value = feature_entry.second; |
| String endpoint; |
| |
| if (!value.params.empty()) { |
| for (const auto& param : value.params) { |
| if (param.first == "report-to" && param.second.is_token()) { |
| endpoint = String(param.second.GetString()); |
| } |
| } |
| } |
| |
| Vector<String> allowlist; |
| for (const auto& parameterized_item : value.member) { |
| if (!parameterized_item.params.empty()) { |
| logger_.Warn(UNSAFE_TODO(String::Format( |
| "Feature %s's parameters are ignored.", feature_name))); |
| } |
| |
| String allowlist_item; |
| if (parameterized_item.item.is_token()) { |
| // All special keyword appears as token, i.e. self, src and *. |
| const std::string& token_value = parameterized_item.item.GetString(); |
| if (token_value != "*" && token_value != "self") { |
| logger_.Warn(UNSAFE_TODO(String::Format( |
| "Invalid allowlist item(%s) for feature %s. Allowlist item " |
| "must be *, self or quoted url.", |
| token_value.c_str(), feature_name))); |
| continue; |
| } |
| |
| if (token_value == "*") { |
| allowlist_item = "*"; |
| } else { |
| allowlist_item = String::Format("'%s'", token_value.c_str()); |
| } |
| } else if (parameterized_item.item.is_string()) { |
| allowlist_item = parameterized_item.item.GetString().c_str(); |
| } else { |
| logger_.Warn(UNSAFE_TODO( |
| String::Format("Invalid allowlist item for feature %s. Allowlist " |
| "item must be *, self, or quoted url.", |
| feature_name))); |
| continue; |
| } |
| if (!allowlist_item.empty()) { |
| allowlist.push_back(allowlist_item); |
| } |
| } |
| |
| if (allowlist.empty()) { |
| allowlist.push_back("'none'"); |
| } |
| |
| ir_root.declarations.push_back(PermissionsPolicyParser::Declaration{ |
| feature_name, std::move(allowlist), endpoint}); |
| } |
| |
| return ir_root; |
| } |
| |
| // This merges the permissions policies defined within the Isolated Web App |
| // manifest with the ones received from headers of the particular page. The |
| // general mechanism and rationale behind it is explained in more detail here: |
| // https://github.com/WICG/isolated-web-apps/blob/main/Permissions.md#proposal |
| // |
| // In short, the mechanism is as follows: |
| // If the feature: |
| // - Doesn't have nonempty allowlist defined within the manifest, any potential |
| // mentions of it in headers are skipped (and its usage is not allowed). |
| // - Has an allowlist within the manifest but does not appear at all in headers, |
| // unmodified allowlist from the manifest is used. |
| // - Appears in both the manifest and headers, the intersection of both |
| // allowlists is used. |
| // For example, if the manifest specifies: |
| // direct-sockets: self origin1 origin2 |
| // and headers specify: |
| // direct-sockets: self, origin2, origin3 |
| // the merged allowlist will look: |
| // direct-sockets: self, origin2 |
| // |
| // This means that: |
| // - `base_policy` here comes from the manifest. |
| // - `header_policy` here comes from the headers. |
| // - Headers can only limit the allowlists extracted from the manifest, never |
| // extend them. |
| network::ParsedPermissionsPolicy CombinePermissionsPolicies( |
| const network::ParsedPermissionsPolicy& base_policy, |
| const network::ParsedPermissionsPolicy& header_policy) { |
| auto result = base_policy; |
| for (const network::ParsedPermissionsPolicyDeclaration& |
| declaration_in_headers : header_policy) { |
| // If the header policy allows all origins, it doesn't restrict the base |
| // policy. |
| if (declaration_in_headers.matches_all_origins) { |
| continue; |
| } |
| auto base_declaration = std::ranges::find( |
| result, declaration_in_headers.feature, |
| &network::ParsedPermissionsPolicyDeclaration::feature); |
| if (base_declaration == result.end()) { |
| continue; |
| } |
| |
| // If the base policy allows all origins, we simply replace it with the |
| // header policy (which is more restrictive). |
| if (base_declaration->matches_all_origins) { |
| base_declaration->matches_all_origins = false; |
| base_declaration->allowed_origins = |
| declaration_in_headers.allowed_origins; |
| base_declaration->self_if_matches = |
| declaration_in_headers.self_if_matches; |
| base_declaration->matches_opaque_src = |
| declaration_in_headers.matches_opaque_src; |
| continue; |
| } |
| |
| auto allowed_origins_headers = declaration_in_headers.allowed_origins; |
| std::ranges::sort(allowed_origins_headers); |
| |
| auto allowed_origins_manifest = base_declaration->allowed_origins; |
| std::ranges::sort(allowed_origins_manifest); |
| |
| // Intersect the allowed origins from the manifest and the header. |
| std::vector<network::OriginWithPossibleWildcards> allowed_origins; |
| std::ranges::set_intersection(allowed_origins_headers, |
| allowed_origins_manifest, |
| std::back_inserter(allowed_origins)); |
| |
| base_declaration->allowed_origins = std::move(allowed_origins); |
| |
| // If the base policy allows 'self', we must check if the header policy also |
| // allows 'self'. If not, 'self' is removed from the allowlist. |
| if (base_declaration->self_if_matches) { |
| // `self_if_matches` in both places is an optional that can be either |
| // `std::nullopt` or the origin of this IWA, nothing else. |
| base_declaration->self_if_matches = |
| declaration_in_headers.self_if_matches; |
| } |
| |
| if (base_declaration->matches_opaque_src) { |
| base_declaration->matches_opaque_src = |
| declaration_in_headers.matches_opaque_src; |
| } |
| |
| // Reporting endpoint cannot be specified within the manifest, the one from |
| // headers is used. |
| base_declaration->reporting_endpoint = |
| declaration_in_headers.reporting_endpoint; |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| network::ParsedPermissionsPolicy |
| PermissionsPolicyParser::ParseIsolatedAppPermissionsPolicy( |
| const Vector<IsolatedAppPermissionPolicyEntry>& isolated_app_policy, |
| const network::ParsedPermissionsPolicy& permissions_policy_from_headers, |
| const SecurityOrigin& origin, |
| PolicyParserMessageBuffer& permissions_policy_logger, |
| ExecutionContext* execution_context) { |
| if (isolated_app_policy.empty()) { |
| return {}; |
| } |
| |
| Node node = { |
| .type = network::OriginWithPossibleWildcards::NodeType::kHeader, |
| .declarations{ |
| isolated_app_policy, [](const auto& entry) -> Declaration { |
| return { |
| .feature_name = entry.feature, |
| // can't use = here because the constructor we need is explicit |
| .allowlist{entry.allowed_origins}, |
| }; |
| }}}; |
| |
| return CombinePermissionsPolicies( |
| ParsePolicyFromNode(node, origin, permissions_policy_logger, |
| execution_context), |
| permissions_policy_from_headers); |
| } |
| |
| network::ParsedPermissionsPolicy PermissionsPolicyParser::ParseHeader( |
| const String& feature_policy_header, |
| const String& permissions_policy_header, |
| const SecurityOrigin& origin, |
| PolicyParserMessageBuffer& feature_policy_logger, |
| PolicyParserMessageBuffer& permissions_policy_logger, |
| ExecutionContext* execution_context) { |
| bool is_isolated_context = |
| execution_context && execution_context->IsIsolatedContext(); |
| network::ParsedPermissionsPolicy permissions_policy = |
| ParsingContext(permissions_policy_logger, origin, nullptr, |
| GetDefaultFeatureNameMap(is_isolated_context), |
| execution_context) |
| .ParsePermissionsPolicy(permissions_policy_header); |
| network::ParsedPermissionsPolicy feature_policy = |
| ParsingContext(feature_policy_logger, origin, nullptr, |
| GetDefaultFeatureNameMap(is_isolated_context), |
| execution_context) |
| .ParseFeaturePolicy(feature_policy_header); |
| |
| FeatureObserver observer; |
| for (const auto& policy_declaration : permissions_policy) { |
| bool feature_observed = |
| observer.FeatureObserved(policy_declaration.feature); |
| DCHECK(!feature_observed); |
| } |
| |
| std::vector<std::string> overlap_features; |
| |
| for (const auto& policy_declaration : feature_policy) { |
| if (!observer.FeatureObserved(policy_declaration.feature)) { |
| permissions_policy.push_back(policy_declaration); |
| } else { |
| overlap_features.push_back( |
| GetNameForFeature(policy_declaration.feature, is_isolated_context) |
| .Ascii()); |
| } |
| } |
| |
| if (!overlap_features.empty()) { |
| std::ostringstream features_stream; |
| std::copy(overlap_features.begin(), overlap_features.end() - 1, |
| std::ostream_iterator<std::string>(features_stream, ", ")); |
| features_stream << overlap_features.back(); |
| |
| feature_policy_logger.Warn(String::Format( |
| "Some features are specified in both Feature-Policy and " |
| "Permissions-Policy header: %s. Values defined in Permissions-Policy " |
| "header will be used.", |
| features_stream.str().c_str())); |
| } |
| return permissions_policy; |
| } |
| |
| network::ParsedPermissionsPolicy PermissionsPolicyParser::ParseAttribute( |
| const String& policy, |
| const SecurityOrigin& self_origin, |
| const SecurityOrigin& src_origin, |
| PolicyParserMessageBuffer& logger, |
| ExecutionContext* execution_context) { |
| bool is_isolated_context = |
| execution_context && execution_context->IsIsolatedContext(); |
| return ParsingContext(logger, self_origin, &src_origin, |
| GetDefaultFeatureNameMap(is_isolated_context), |
| execution_context) |
| .ParseFeaturePolicy(policy); |
| } |
| |
| network::ParsedPermissionsPolicy PermissionsPolicyParser::ParsePolicyFromNode( |
| PermissionsPolicyParser::Node& policy, |
| const SecurityOrigin& origin, |
| PolicyParserMessageBuffer& logger, |
| ExecutionContext* execution_context) { |
| bool is_isolated_context = |
| execution_context && execution_context->IsIsolatedContext(); |
| return ParsingContext(logger, origin, /*src_origin=*/nullptr, |
| GetDefaultFeatureNameMap(is_isolated_context), |
| execution_context) |
| .ParsePolicyFromNode(policy); |
| } |
| |
| network::ParsedPermissionsPolicy |
| PermissionsPolicyParser::ParseFeaturePolicyForTest( |
| const String& policy, |
| const SecurityOrigin& self_origin, |
| const SecurityOrigin* src_origin, |
| PolicyParserMessageBuffer& logger, |
| const FeatureNameMap& feature_names, |
| ExecutionContext* execution_context) { |
| return ParsingContext(logger, self_origin, src_origin, feature_names, |
| execution_context) |
| .ParseFeaturePolicy(policy); |
| } |
| |
| network::ParsedPermissionsPolicy |
| PermissionsPolicyParser::ParsePermissionsPolicyForTest( |
| const String& policy, |
| const SecurityOrigin& self_origin, |
| const SecurityOrigin* src_origin, |
| PolicyParserMessageBuffer& logger, |
| const FeatureNameMap& feature_names, |
| ExecutionContext* execution_context) { |
| return ParsingContext(logger, self_origin, src_origin, feature_names, |
| execution_context) |
| .ParsePermissionsPolicy(policy); |
| } |
| |
| bool IsFeatureDeclared(network::mojom::PermissionsPolicyFeature feature, |
| const network::ParsedPermissionsPolicy& policy) { |
| return std::ranges::contains( |
| policy, feature, &network::ParsedPermissionsPolicyDeclaration::feature); |
| } |
| |
| bool RemoveFeatureIfPresent(network::mojom::PermissionsPolicyFeature feature, |
| network::ParsedPermissionsPolicy& 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( |
| network::mojom::PermissionsPolicyFeature feature, |
| network::ParsedPermissionsPolicy& policy) { |
| if (IsFeatureDeclared(feature, policy)) { |
| return false; |
| } |
| network::ParsedPermissionsPolicyDeclaration allowlist(feature); |
| policy.push_back(allowlist); |
| return true; |
| } |
| |
| bool AllowFeatureEverywhereIfNotPresent( |
| network::mojom::PermissionsPolicyFeature feature, |
| network::ParsedPermissionsPolicy& policy) { |
| if (IsFeatureDeclared(feature, policy)) { |
| return false; |
| } |
| network::ParsedPermissionsPolicyDeclaration allowlist(feature); |
| allowlist.matches_all_origins = true; |
| allowlist.matches_opaque_src = true; |
| policy.push_back(allowlist); |
| return true; |
| } |
| |
| void DisallowFeature(network::mojom::PermissionsPolicyFeature feature, |
| network::ParsedPermissionsPolicy& policy) { |
| RemoveFeatureIfPresent(feature, policy); |
| DisallowFeatureIfNotPresent(feature, policy); |
| } |
| |
| bool IsFeatureForMeasurementOnly( |
| network::mojom::PermissionsPolicyFeature feature) { |
| return feature == network::mojom::PermissionsPolicyFeature::kWebShare; |
| } |
| |
| void AllowFeatureEverywhere(network::mojom::PermissionsPolicyFeature feature, |
| network::ParsedPermissionsPolicy& policy) { |
| RemoveFeatureIfPresent(feature, policy); |
| AllowFeatureEverywhereIfNotPresent(feature, policy); |
| } |
| |
| const Vector<String> GetAvailableFeatures(ExecutionContext* execution_context) { |
| Vector<String> available_features; |
| bool is_isolated_context = |
| execution_context && execution_context->IsIsolatedContext(); |
| for (const auto& feature : GetDefaultFeatureNameMap(is_isolated_context)) { |
| if (!DisabledByOriginTrial(feature.key, execution_context) && |
| !IsFeatureForMeasurementOnly(feature.value)) { |
| available_features.push_back(feature.key); |
| } |
| } |
| return available_features; |
| } |
| |
| const String GetNameForFeature(network::mojom::PermissionsPolicyFeature feature, |
| bool is_isolated_context) { |
| for (const auto& entry : GetDefaultFeatureNameMap(is_isolated_context)) { |
| if (entry.value == feature) { |
| return entry.key; |
| } |
| } |
| return g_empty_string; |
| } |
| |
| } // namespace blink |