1 // Copyright 2017 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/reporting/reporting_header_parser.h"
6
7 #include <cstring>
8 #include <string>
9 #include <utility>
10 #include <vector>
11
12 #include "base/check.h"
13 #include "base/feature_list.h"
14 #include "base/functional/bind.h"
15 #include "base/json/json_reader.h"
16 #include "base/metrics/histogram_functions.h"
17 #include "base/time/time.h"
18 #include "base/values.h"
19 #include "net/base/features.h"
20 #include "net/base/isolation_info.h"
21 #include "net/base/network_anonymization_key.h"
22 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
23 #include "net/reporting/reporting_cache.h"
24 #include "net/reporting/reporting_context.h"
25 #include "net/reporting/reporting_delegate.h"
26 #include "net/reporting/reporting_endpoint.h"
27 #include "net/reporting/reporting_target_type.h"
28
29 namespace net {
30
31 namespace {
32
33 const char kUrlKey[] = "url";
34 const char kIncludeSubdomainsKey[] = "include_subdomains";
35 const char kEndpointsKey[] = "endpoints";
36 const char kGroupKey[] = "group";
37 const char kDefaultGroupName[] = "default";
38 const char kMaxAgeKey[] = "max_age";
39 const char kPriorityKey[] = "priority";
40 const char kWeightKey[] = "weight";
41
42 // Processes a single endpoint url string parsed from header.
43 //
44 // |endpoint_url_string| is the string value of the endpoint URL.
45 // |header_origin_url| is the origin URL that sent the header.
46 //
47 // |endpoint_url_out| is the endpoint URL parsed out of the string.
48 // Returns true on success or false if url was invalid.
ProcessEndpointURLString(const std::string & endpoint_url_string,const url::Origin & header_origin,GURL & endpoint_url_out)49 bool ProcessEndpointURLString(const std::string& endpoint_url_string,
50 const url::Origin& header_origin,
51 GURL& endpoint_url_out) {
52 // Support path-absolute-URL string with exactly one leading "/"
53 if (std::strspn(endpoint_url_string.c_str(), "/") == 1) {
54 endpoint_url_out = header_origin.GetURL().Resolve(endpoint_url_string);
55 } else {
56 endpoint_url_out = GURL(endpoint_url_string);
57 }
58 if (!endpoint_url_out.is_valid())
59 return false;
60 if (!endpoint_url_out.SchemeIsCryptographic())
61 return false;
62 return true;
63 }
64
65 // Processes a single endpoint tuple received in a Report-To header.
66 //
67 // |origin| is the origin that sent the Report-To header.
68 //
69 // |value| is the parsed JSON value of the endpoint tuple.
70 //
71 // |*endpoint_info_out| will contain the endpoint URL parsed out of the tuple.
72 // Returns true on success or false if endpoint was discarded.
ProcessEndpoint(ReportingDelegate * delegate,const ReportingEndpointGroupKey & group_key,const base::Value & value,ReportingEndpoint::EndpointInfo * endpoint_info_out)73 bool ProcessEndpoint(ReportingDelegate* delegate,
74 const ReportingEndpointGroupKey& group_key,
75 const base::Value& value,
76 ReportingEndpoint::EndpointInfo* endpoint_info_out) {
77 const base::Value::Dict* dict = value.GetIfDict();
78 if (!dict)
79 return false;
80
81 const std::string* endpoint_url_string = dict->FindString(kUrlKey);
82 if (!endpoint_url_string)
83 return false;
84
85 GURL endpoint_url;
86 // V0 endpoints should have an origin.
87 DCHECK(group_key.origin.has_value());
88 if (!ProcessEndpointURLString(*endpoint_url_string, group_key.origin.value(),
89 endpoint_url)) {
90 return false;
91 }
92 endpoint_info_out->url = std::move(endpoint_url);
93
94 int priority = ReportingEndpoint::EndpointInfo::kDefaultPriority;
95 if (const base::Value* priority_value = dict->Find(kPriorityKey)) {
96 if (!priority_value->is_int())
97 return false;
98 priority = priority_value->GetInt();
99 }
100 if (priority < 0)
101 return false;
102 endpoint_info_out->priority = priority;
103
104 int weight = ReportingEndpoint::EndpointInfo::kDefaultWeight;
105 if (const base::Value* weight_value = dict->Find(kWeightKey)) {
106 if (!weight_value->is_int())
107 return false;
108 weight = weight_value->GetInt();
109 }
110 if (weight < 0)
111 return false;
112 endpoint_info_out->weight = weight;
113
114 return delegate->CanSetClient(group_key.origin.value(),
115 endpoint_info_out->url);
116 }
117
118 // Processes a single endpoint group tuple received in a Report-To header.
119 //
120 // |origin| is the origin that sent the Report-To header.
121 //
122 // |value| is the parsed JSON value of the endpoint group tuple.
123 // Returns true on successfully adding a non-empty group, or false if endpoint
124 // group was discarded or processed as a deletion.
ProcessEndpointGroup(ReportingDelegate * delegate,ReportingCache * cache,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,const base::Value & value,ReportingEndpointGroup * parsed_endpoint_group_out)125 bool ProcessEndpointGroup(
126 ReportingDelegate* delegate,
127 ReportingCache* cache,
128 const NetworkAnonymizationKey& network_anonymization_key,
129 const url::Origin& origin,
130 const base::Value& value,
131 ReportingEndpointGroup* parsed_endpoint_group_out) {
132 const base::Value::Dict* dict = value.GetIfDict();
133 if (!dict)
134 return false;
135
136 std::string group_name = kDefaultGroupName;
137 if (const base::Value* maybe_group_name = dict->Find(kGroupKey)) {
138 if (!maybe_group_name->is_string())
139 return false;
140 group_name = maybe_group_name->GetString();
141 }
142 // The target_type is set to kDeveloper because enterprise endpoints are
143 // created on a different path.
144 ReportingEndpointGroupKey group_key(network_anonymization_key, origin,
145 group_name,
146 ReportingTargetType::kDeveloper);
147 parsed_endpoint_group_out->group_key = group_key;
148
149 int ttl_sec = dict->FindInt(kMaxAgeKey).value_or(-1);
150 if (ttl_sec < 0)
151 return false;
152 // max_age: 0 signifies removal of the endpoint group.
153 if (ttl_sec == 0) {
154 cache->RemoveEndpointGroup(group_key);
155 return false;
156 }
157 parsed_endpoint_group_out->ttl = base::Seconds(ttl_sec);
158
159 std::optional<bool> subdomains_bool = dict->FindBool(kIncludeSubdomainsKey);
160 if (subdomains_bool && subdomains_bool.value()) {
161 // Disallow eTLDs from setting include_subdomains endpoint groups.
162 if (registry_controlled_domains::GetRegistryLength(
163 origin.GetURL(),
164 registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
165 registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) == 0) {
166 return false;
167 }
168
169 parsed_endpoint_group_out->include_subdomains = OriginSubdomains::INCLUDE;
170 }
171
172 const base::Value::List* endpoint_list = dict->FindList(kEndpointsKey);
173 if (!endpoint_list)
174 return false;
175
176 std::vector<ReportingEndpoint::EndpointInfo> endpoints;
177
178 for (const base::Value& endpoint : *endpoint_list) {
179 ReportingEndpoint::EndpointInfo parsed_endpoint;
180 if (ProcessEndpoint(delegate, group_key, endpoint, &parsed_endpoint))
181 endpoints.push_back(std::move(parsed_endpoint));
182 }
183
184 // Remove the group if it is empty.
185 if (endpoints.empty()) {
186 cache->RemoveEndpointGroup(group_key);
187 return false;
188 }
189
190 parsed_endpoint_group_out->endpoints = std::move(endpoints);
191
192 return true;
193 }
194
195 // Processes a single endpoint tuple received in a Reporting-Endpoints header.
196 //
197 // |group_key| is the key for the endpoint group this endpoint belongs.
198 // |endpoint_url_string| is the endpoint url as received in the header.
199 //
200 // |endpoint_info_out| is the endpoint info parsed out of the value.
ProcessEndpoint(ReportingDelegate * delegate,const ReportingEndpointGroupKey & group_key,const std::string & endpoint_url_string,ReportingEndpoint::EndpointInfo & endpoint_info_out)201 bool ProcessEndpoint(ReportingDelegate* delegate,
202 const ReportingEndpointGroupKey& group_key,
203 const std::string& endpoint_url_string,
204 ReportingEndpoint::EndpointInfo& endpoint_info_out) {
205 if (endpoint_url_string.empty())
206 return false;
207
208 GURL endpoint_url;
209 // Document endpoints should have an origin.
210 DCHECK(group_key.origin.has_value());
211 if (!ProcessEndpointURLString(endpoint_url_string, group_key.origin.value(),
212 endpoint_url)) {
213 return false;
214 }
215 endpoint_info_out.url = std::move(endpoint_url);
216 // Reporting-Endpoints endpoint doesn't have prioirty/weight so set to
217 // default.
218 endpoint_info_out.priority =
219 ReportingEndpoint::EndpointInfo::kDefaultPriority;
220 endpoint_info_out.weight = ReportingEndpoint::EndpointInfo::kDefaultWeight;
221
222 return delegate->CanSetClient(group_key.origin.value(),
223 endpoint_info_out.url);
224 }
225
226 // Process a single endpoint received in a Reporting-Endpoints header.
ProcessV1Endpoint(ReportingDelegate * delegate,ReportingCache * cache,const base::UnguessableToken & reporting_source,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,const std::string & endpoint_name,const std::string & endpoint_url_string,ReportingEndpoint & parsed_endpoint_out)227 bool ProcessV1Endpoint(ReportingDelegate* delegate,
228 ReportingCache* cache,
229 const base::UnguessableToken& reporting_source,
230 const NetworkAnonymizationKey& network_anonymization_key,
231 const url::Origin& origin,
232 const std::string& endpoint_name,
233 const std::string& endpoint_url_string,
234 ReportingEndpoint& parsed_endpoint_out) {
235 DCHECK(!reporting_source.is_empty());
236 // The target_type is set to kDeveloper because enterprise endpoints are
237 // created on a different path.
238 ReportingEndpointGroupKey group_key(network_anonymization_key,
239 reporting_source, origin, endpoint_name,
240 ReportingTargetType::kDeveloper);
241 parsed_endpoint_out.group_key = group_key;
242
243 ReportingEndpoint::EndpointInfo parsed_endpoint;
244
245 if (!ProcessEndpoint(delegate, group_key, endpoint_url_string,
246 parsed_endpoint)) {
247 return false;
248 }
249 parsed_endpoint_out.info = std::move(parsed_endpoint);
250 return true;
251 }
252
253 } // namespace
254
ParseReportingEndpoints(const std::string & header)255 std::optional<base::flat_map<std::string, std::string>> ParseReportingEndpoints(
256 const std::string& header) {
257 // Ignore empty header values. Skip logging metric to maintain parity with
258 // ReportingHeaderType::kReportToInvalid.
259 if (header.empty())
260 return std::nullopt;
261 std::optional<structured_headers::Dictionary> header_dict =
262 structured_headers::ParseDictionary(header);
263 if (!header_dict) {
264 ReportingHeaderParser::RecordReportingHeaderType(
265 ReportingHeaderParser::ReportingHeaderType::kReportingEndpointsInvalid);
266 return std::nullopt;
267 }
268 base::flat_map<std::string, std::string> parsed_header;
269 for (const structured_headers::DictionaryMember& entry : *header_dict) {
270 if (entry.second.member_is_inner_list ||
271 !entry.second.member.front().item.is_string()) {
272 ReportingHeaderParser::RecordReportingHeaderType(
273 ReportingHeaderParser::ReportingHeaderType::
274 kReportingEndpointsInvalid);
275 return std::nullopt;
276 }
277 const std::string& endpoint_url_string =
278 entry.second.member.front().item.GetString();
279 parsed_header[entry.first] = endpoint_url_string;
280 }
281 return parsed_header;
282 }
283
284 // static
RecordReportingHeaderType(ReportingHeaderType header_type)285 void ReportingHeaderParser::RecordReportingHeaderType(
286 ReportingHeaderType header_type) {
287 base::UmaHistogramEnumeration("Net.Reporting.HeaderType", header_type);
288 }
289
290 // static
ParseReportToHeader(ReportingContext * context,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,const base::Value::List & list)291 void ReportingHeaderParser::ParseReportToHeader(
292 ReportingContext* context,
293 const NetworkAnonymizationKey& network_anonymization_key,
294 const url::Origin& origin,
295 const base::Value::List& list) {
296 DCHECK(GURL::SchemeIsCryptographic(origin.scheme()));
297
298 ReportingDelegate* delegate = context->delegate();
299 ReportingCache* cache = context->cache();
300
301 std::vector<ReportingEndpointGroup> parsed_header;
302
303 for (const auto& group_value : list) {
304 ReportingEndpointGroup parsed_endpoint_group;
305 if (ProcessEndpointGroup(delegate, cache, network_anonymization_key, origin,
306 group_value, &parsed_endpoint_group)) {
307 parsed_header.push_back(std::move(parsed_endpoint_group));
308 }
309 }
310
311 if (parsed_header.empty() && list.size() > 0) {
312 RecordReportingHeaderType(ReportingHeaderType::kReportToInvalid);
313 }
314
315 // Remove the client if it has no valid endpoint groups.
316 if (parsed_header.empty()) {
317 cache->RemoveClient(network_anonymization_key, origin);
318 return;
319 }
320
321 RecordReportingHeaderType(ReportingHeaderType::kReportTo);
322
323 cache->OnParsedHeader(network_anonymization_key, origin,
324 std::move(parsed_header));
325 }
326
327 // static
ProcessParsedReportingEndpointsHeader(ReportingContext * context,const base::UnguessableToken & reporting_source,const IsolationInfo & isolation_info,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,base::flat_map<std::string,std::string> header)328 void ReportingHeaderParser::ProcessParsedReportingEndpointsHeader(
329 ReportingContext* context,
330 const base::UnguessableToken& reporting_source,
331 const IsolationInfo& isolation_info,
332 const NetworkAnonymizationKey& network_anonymization_key,
333 const url::Origin& origin,
334 base::flat_map<std::string, std::string> header) {
335 DCHECK(base::FeatureList::IsEnabled(net::features::kDocumentReporting));
336 DCHECK(GURL::SchemeIsCryptographic(origin.scheme()));
337 DCHECK(!reporting_source.is_empty());
338 DCHECK(network_anonymization_key.IsEmpty() ||
339 network_anonymization_key ==
340 isolation_info.network_anonymization_key());
341
342 ReportingDelegate* delegate = context->delegate();
343 ReportingCache* cache = context->cache();
344
345 std::vector<ReportingEndpoint> parsed_header;
346
347 for (const auto& member : header) {
348 ReportingEndpoint parsed_endpoint;
349 if (ProcessV1Endpoint(delegate, cache, reporting_source,
350 network_anonymization_key, origin, member.first,
351 member.second, parsed_endpoint)) {
352 parsed_header.push_back(std::move(parsed_endpoint));
353 }
354 }
355
356 if (parsed_header.empty()) {
357 RecordReportingHeaderType(ReportingHeaderType::kReportingEndpointsInvalid);
358 return;
359 }
360
361 RecordReportingHeaderType(ReportingHeaderType::kReportingEndpoints);
362 cache->OnParsedReportingEndpointsHeader(reporting_source, isolation_info,
363 std::move(parsed_header));
364 }
365
366 } // namespace net
367