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