1use std::borrow::Cow;
6
7pub use content_security_policy::InlineCheckType;
9pub use content_security_policy::Violation;
11use content_security_policy::{
12 CheckResult, CspList, Destination, Element as CspElement, Initiator, NavigationCheckType,
13 Origin, ParserMetadata, PolicyDisposition, PolicySource, Request, Response as CspResponse,
14 ViolationResource,
15};
16use http::header::{HeaderMap, HeaderValue, ValueIter};
17use hyper_serde::Serde;
18use js::context::JSContext;
19use js::realm::CurrentRealm;
20use js::rust::describe_scripted_caller;
21use log::warn;
22use servo_constellation_traits::{LoadData, LoadOrigin};
23use url::Url;
24
25use super::csppolicyviolationreport::CSPViolationReportBuilder;
26use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
27use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
28use crate::dom::bindings::inheritance::Castable;
29use crate::dom::bindings::refcounted::Trusted;
30use crate::dom::bindings::root::DomRoot;
31use crate::dom::element::Element;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::node::{Node, NodeTraits};
34use crate::dom::reporting::reportingobserver::ReportingObserver;
35use crate::dom::security::cspviolationreporttask::CSPViolationReportTask;
36use crate::dom::trustedtypes::trustedscript::TrustedScript;
37use crate::dom::window::Window;
38use crate::tasks::task::TaskOnce;
39
40pub(crate) trait CspReporting {
41 fn is_js_evaluation_allowed(
42 &self,
43 cx: &mut JSContext,
44 global: &GlobalScope,
45 source: &str,
46 ) -> bool;
47 fn is_wasm_evaluation_allowed(&self, cx: &mut JSContext, global: &GlobalScope) -> bool;
48 fn should_navigation_request_be_blocked(
49 &self,
50 cx: &mut JSContext,
51 global: &GlobalScope,
52 load_data: &mut LoadData,
53 element: Option<&Element>,
54 ) -> bool;
55 fn should_navigation_response_to_navigation_request_be_blocked(
56 &self,
57 cx: &mut JSContext,
58 window: &Window,
59 url: Url,
60 self_origin: &url::Origin,
61 ) -> bool;
62 fn should_elements_inline_type_behavior_be_blocked(
63 &self,
64 cx: &mut JSContext,
65 global: &GlobalScope,
66 el: &Element,
67 type_: InlineCheckType,
68 source: &str,
69 current_line: u32,
70 ) -> bool;
71 fn is_trusted_type_policy_creation_allowed(
72 &self,
73 cx: &mut JSContext,
74 global: &GlobalScope,
75 policy_name: &str,
76 created_policy_names: &[&str],
77 ) -> bool;
78 fn does_sink_type_require_trusted_types(
79 &self,
80 sink_group: &str,
81 include_report_only_policies: bool,
82 ) -> bool;
83 fn should_sink_type_mismatch_violation_be_blocked_by_csp(
84 &self,
85 cx: &mut JSContext,
86 global: &GlobalScope,
87 sink: &str,
88 sink_group: &str,
89 source: &str,
90 ) -> bool;
91 fn is_base_allowed_for_document(
92 &self,
93 cx: &mut JSContext,
94 global: &GlobalScope,
95 base: &url::Url,
96 self_origin: &url::Origin,
97 ) -> bool;
98 fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList>;
99}
100
101impl CspReporting for Option<CspList> {
102 fn is_js_evaluation_allowed(
104 &self,
105 cx: &mut JSContext,
106 global: &GlobalScope,
107 source: &str,
108 ) -> bool {
109 let Some(csp_list) = self else {
110 return true;
111 };
112
113 let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source);
114
115 global.report_csp_violations(cx, violations, None, None);
116
117 is_js_evaluation_allowed == CheckResult::Allowed
118 }
119
120 fn is_wasm_evaluation_allowed(&self, cx: &mut JSContext, global: &GlobalScope) -> bool {
122 let Some(csp_list) = self else {
123 return true;
124 };
125
126 let (is_wasm_evaluation_allowed, violations) = csp_list.is_wasm_evaluation_allowed();
127
128 global.report_csp_violations(cx, violations, None, None);
129
130 is_wasm_evaluation_allowed == CheckResult::Allowed
131 }
132
133 fn should_navigation_request_be_blocked(
135 &self,
136 cx: &mut JSContext,
137 global: &GlobalScope,
138 load_data: &mut LoadData,
139 element: Option<&Element>,
140 ) -> bool {
141 let Some(csp_list) = self else {
142 return false;
143 };
144 let mut request = Request {
145 url: load_data.url.clone().into_url(),
146 current_url: load_data.url.clone().into_url(),
148 origin: match &load_data.load_origin {
149 LoadOrigin::Script(origin) => origin.immutable().clone().into_url_origin(),
150 _ => Origin::new_opaque(),
151 },
152 redirect_count: 0,
154 destination: Destination::None,
155 initiator: Initiator::None,
156 nonce: String::new(),
157 integrity_metadata: String::new(),
158 parser_metadata: ParserMetadata::None,
159 };
160 let (result, violations) = csp_list.should_navigation_request_be_blocked(
162 &mut request,
163 NavigationCheckType::Other,
164 |script_source| {
165 TrustedScript::get_trusted_type_compliant_string(
168 cx,
169 global,
170 TrustedScriptOrString::String(script_source.into()),
171 "Location href",
172 )
173 .ok()
174 .map(|s| s.into())
175 },
176 );
177
178 load_data.url = request.url.into();
180
181 global.report_csp_violations(cx, violations, element, None);
182
183 result == CheckResult::Blocked
184 }
185
186 fn should_navigation_response_to_navigation_request_be_blocked(
188 &self,
189 cx: &mut JSContext,
190 window: &Window,
191 url: Url,
192 self_origin: &url::Origin,
193 ) -> bool {
194 let Some(csp_list) = self else {
195 return false;
196 };
197
198 let mut window_proxy = window.window_proxy();
199 let mut parent_navigable_origins = vec![];
200 loop {
201 if let Some(container_element) = window_proxy.frame_element() {
203 let container_document = container_element.owner_document();
204 let parent_origin = Url::parse(
205 container_document
206 .origin()
207 .immutable()
208 .ascii_serialization()
209 .as_ref(),
210 )
211 .expect("Must always be able to parse document origin");
212 parent_navigable_origins.push(parent_origin);
213 window_proxy = container_document.window().window_proxy();
214 continue;
215 }
216 if let Some(parent_proxy) = window_proxy.parent() {
218 let Some((parent_origin, _)) =
219 parent_proxy.document_origin_and_internal_ancestor_origin_objects_list()
220 else {
221 break;
222 };
223 let parent_origin = parent_origin.immutable().ascii_serialization();
224 let parent_origin = Url::parse(&parent_origin)
225 .expect("Must always be able to parse document origin");
226 parent_navigable_origins.push(parent_origin);
227 window_proxy = DomRoot::from_ref(parent_proxy);
228 continue;
229 }
230 break;
232 }
233
234 let (is_navigation_response_blocked, violations) = csp_list
235 .should_navigation_response_to_navigation_request_be_blocked(
236 &CspResponse {
237 url,
238 redirect_count: 0,
239 },
240 self_origin,
241 &parent_navigable_origins,
242 );
243
244 window
245 .as_global_scope()
246 .report_csp_violations(cx, violations, None, None);
247
248 is_navigation_response_blocked == CheckResult::Blocked
249 }
250
251 fn should_elements_inline_type_behavior_be_blocked(
253 &self,
254 cx: &mut JSContext,
255 global: &GlobalScope,
256 el: &Element,
257 type_: InlineCheckType,
258 source: &str,
259 current_line: u32,
260 ) -> bool {
261 let Some(csp_list) = self else {
262 return false;
263 };
264 let element = CspElement {
265 nonce: if el.is_nonceable() {
266 Some(Cow::Owned(el.nonce_value().trim().to_owned()))
267 } else {
268 None
269 },
270 };
271 let (result, violations) =
272 csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
273
274 let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
275
276 global.report_csp_violations(cx, violations, Some(el), Some(source_position));
277
278 result == CheckResult::Blocked
279 }
280
281 fn is_trusted_type_policy_creation_allowed(
283 &self,
284 cx: &mut JSContext,
285 global: &GlobalScope,
286 policy_name: &str,
287 created_policy_names: &[&str],
288 ) -> bool {
289 let Some(csp_list) = self else {
290 return true;
291 };
292
293 let (allowed_by_csp, violations) =
294 csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
295
296 global.report_csp_violations(cx, violations, None, None);
297
298 allowed_by_csp == CheckResult::Allowed
299 }
300
301 fn does_sink_type_require_trusted_types(
303 &self,
304 sink_group: &str,
305 include_report_only_policies: bool,
306 ) -> bool {
307 let Some(csp_list) = self else {
308 return false;
309 };
310
311 csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
312 }
313
314 fn should_sink_type_mismatch_violation_be_blocked_by_csp(
316 &self,
317 cx: &mut JSContext,
318 global: &GlobalScope,
319 sink: &str,
320 sink_group: &str,
321 source: &str,
322 ) -> bool {
323 let Some(csp_list) = self else {
324 return false;
325 };
326
327 let (allowed_by_csp, violations) = csp_list
328 .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
329
330 global.report_csp_violations(cx, violations, None, None);
331
332 allowed_by_csp == CheckResult::Blocked
333 }
334
335 fn is_base_allowed_for_document(
337 &self,
338 cx: &mut JSContext,
339 global: &GlobalScope,
340 base: &url::Url,
341 self_origin: &url::Origin,
342 ) -> bool {
343 let Some(csp_list) = self else {
344 return true;
345 };
346
347 let (is_base_allowed, violations) =
348 csp_list.is_base_allowed_for_document(base, self_origin);
349
350 global.report_csp_violations(cx, violations, None, None);
351
352 is_base_allowed == CheckResult::Allowed
353 }
354
355 fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
356 let Some(new_csp_list) = new_csp_list else {
357 return self;
358 };
359
360 match self {
361 None => Some(new_csp_list),
362 Some(mut old_csp_list) => {
363 old_csp_list.append(new_csp_list);
364 Some(old_csp_list)
365 },
366 }
367 }
368}
369
370pub(crate) struct SourcePosition {
371 pub(crate) source_file: String,
372 pub(crate) line_number: u32,
373 pub(crate) column_number: u32,
374}
375
376pub(crate) trait GlobalCspReporting {
377 fn report_csp_violations(
378 &self,
379 cx: &mut JSContext,
380 violations: Vec<Violation>,
381 element: Option<&Element>,
382 source_position: Option<SourcePosition>,
383 );
384}
385
386fn compute_scripted_caller_source_position(cx: &mut JSContext) -> SourcePosition {
387 match describe_scripted_caller(cx) {
388 Ok(scripted_caller) => SourcePosition {
389 source_file: scripted_caller.filename,
390 line_number: scripted_caller.line,
391 column_number: scripted_caller.col + 1,
392 },
393 Err(()) => SourcePosition {
394 source_file: String::new(),
395 line_number: 0,
396 column_number: 0,
397 },
398 }
399}
400
401fn obtain_blocked_uri_for_violation_resource_with_sample(
403 resource: ViolationResource,
404) -> (Option<String>, String) {
405 match resource {
411 ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
412 ViolationResource::Url(url) => (
414 Some(String::new()),
415 ReportingObserver::strip_url_for_reports(url.into()),
416 ),
417 ViolationResource::TrustedTypePolicy { sample } => {
418 (Some(sample), "trusted-types-policy".to_owned())
419 },
420 ViolationResource::TrustedTypeSink { sample } => {
421 (Some(sample), "trusted-types-sink".to_owned())
422 },
423 ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
424 ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
425 }
426}
427
428fn csp_violation_report_tasks(
429 cx: &mut JSContext,
430 global: &GlobalScope,
431 violations: Vec<Violation>,
432 element: Option<&Element>,
433 source_position: Option<SourcePosition>,
434) -> Vec<CSPViolationReportTask> {
435 if violations.is_empty() {
436 return Vec::new();
437 }
438 warn!("Reporting CSP violations: {:?}", violations);
439 let source_position =
440 source_position.unwrap_or_else(|| compute_scripted_caller_source_position(cx));
441 violations
442 .into_iter()
443 .map(|violation| {
444 let (sample, resource) =
445 obtain_blocked_uri_for_violation_resource_with_sample(violation.resource);
446 let report = CSPViolationReportBuilder::default()
447 .resource(resource)
448 .sample(sample)
449 .effective_directive(violation.directive.name)
450 .original_policy(violation.policy.to_string())
451 .report_only(violation.policy.disposition == PolicyDisposition::Report)
452 .source_file(source_position.source_file.clone())
453 .line_number(source_position.line_number)
454 .column_number(source_position.column_number)
455 .build(global);
456 let target = element.and_then(|event_target| {
460 if let Some(window) = global.downcast::<Window>() {
463 if event_target.upcast::<Node>().owner_document() != window.Document() {
467 return None;
468 }
469 }
470 Some(event_target)
471 });
472 let target = match target {
473 None => {
475 if let Some(window) = global.downcast::<Window>() {
477 Trusted::new(window.Document().upcast())
478 } else {
479 Trusted::new(global.upcast())
481 }
482 },
483 Some(event_target) => Trusted::new(event_target.upcast()),
484 };
485 CSPViolationReportTask::new(Trusted::new(global), target, report, violation.policy)
486 })
487 .collect()
488}
489
490impl GlobalScope {
491 pub(crate) fn run_worker_csp_violation_report_tasks(
492 &self,
493 violations: Vec<Violation>,
494 cx: &mut CurrentRealm,
495 ) {
496 for task in csp_violation_report_tasks(cx, self, violations, None, None) {
500 task.run_once(cx);
501 }
502 }
503}
504
505impl GlobalCspReporting for GlobalScope {
506 fn report_csp_violations(
508 &self,
509 cx: &mut JSContext,
510 violations: Vec<Violation>,
511 element: Option<&Element>,
512 source_position: Option<SourcePosition>,
513 ) {
514 for task in csp_violation_report_tasks(cx, self, violations, element, source_position) {
516 self.task_manager()
517 .dom_manipulation_task_source()
518 .queue(task);
519 }
520 }
521}
522
523fn parse_and_potentially_append_to_csp_list(
524 old_csp_list: Option<CspList>,
525 csp_header_iter: ValueIter<HeaderValue>,
526 disposition: PolicyDisposition,
527) -> Option<CspList> {
528 let mut csp_list = old_csp_list;
529 for header in csp_header_iter {
530 let new_csp_list = header
533 .to_str()
534 .ok()
535 .map(|value| CspList::parse(value, PolicySource::Header, disposition));
536 csp_list = csp_list.concatenate(new_csp_list);
537 }
538 csp_list
539}
540
541pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
543 let headers = headers.as_ref()?;
544 let csp_enforce_list = parse_and_potentially_append_to_csp_list(
545 None,
546 headers.get_all("content-security-policy").iter(),
547 PolicyDisposition::Enforce,
548 );
549
550 parse_and_potentially_append_to_csp_list(
551 csp_enforce_list,
552 headers
553 .get_all("content-security-policy-report-only")
554 .iter(),
555 PolicyDisposition::Report,
556 )
557}