1use std::borrow::Cow;
6
7use constellation_traits::{LoadData, LoadOrigin};
8pub use content_security_policy::InlineCheckType;
10pub use content_security_policy::Violation;
12use content_security_policy::{
13 CheckResult, CspList, Destination, Element as CspElement, Initiator, NavigationCheckType,
14 Origin, ParserMetadata, PolicyDisposition, PolicySource, Request, ViolationResource,
15};
16use http::header::{HeaderMap, HeaderValue, ValueIter};
17use hyper_serde::Serde;
18use js::rust::describe_scripted_caller;
19use log::warn;
20
21use super::csppolicyviolationreport::CSPViolationReportBuilder;
22use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
23use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
24use crate::dom::bindings::inheritance::Castable;
25use crate::dom::bindings::refcounted::Trusted;
26use crate::dom::element::Element;
27use crate::dom::globalscope::GlobalScope;
28use crate::dom::node::{Node, NodeTraits};
29use crate::dom::reporting::reportingobserver::ReportingObserver;
30use crate::dom::security::cspviolationreporttask::CSPViolationReportTask;
31use crate::dom::trustedtypes::trustedscript::TrustedScript;
32use crate::dom::window::Window;
33
34pub(crate) trait CspReporting {
35 fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool;
36 fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool;
37 fn should_navigation_request_be_blocked(
38 &self,
39 cx: &mut js::context::JSContext,
40 global: &GlobalScope,
41 load_data: &mut LoadData,
42 element: Option<&Element>,
43 ) -> bool;
44 fn should_elements_inline_type_behavior_be_blocked(
45 &self,
46 global: &GlobalScope,
47 el: &Element,
48 type_: InlineCheckType,
49 source: &str,
50 current_line: u32,
51 ) -> bool;
52 fn is_trusted_type_policy_creation_allowed(
53 &self,
54 global: &GlobalScope,
55 policy_name: &str,
56 created_policy_names: &[&str],
57 ) -> bool;
58 fn does_sink_type_require_trusted_types(
59 &self,
60 sink_group: &str,
61 include_report_only_policies: bool,
62 ) -> bool;
63 fn should_sink_type_mismatch_violation_be_blocked_by_csp(
64 &self,
65 global: &GlobalScope,
66 sink: &str,
67 sink_group: &str,
68 source: &str,
69 ) -> bool;
70 fn is_base_allowed_for_document(
71 &self,
72 global: &GlobalScope,
73 base: &url::Url,
74 self_origin: &url::Origin,
75 ) -> bool;
76 fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList>;
77}
78
79impl CspReporting for Option<CspList> {
80 fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool {
82 let Some(csp_list) = self else {
83 return true;
84 };
85
86 let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source);
87
88 global.report_csp_violations(violations, None, None);
89
90 is_js_evaluation_allowed == CheckResult::Allowed
91 }
92
93 fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool {
95 let Some(csp_list) = self else {
96 return true;
97 };
98
99 let (is_wasm_evaluation_allowed, violations) = csp_list.is_wasm_evaluation_allowed();
100
101 global.report_csp_violations(violations, None, None);
102
103 is_wasm_evaluation_allowed == CheckResult::Allowed
104 }
105
106 fn should_navigation_request_be_blocked(
108 &self,
109 cx: &mut js::context::JSContext,
110 global: &GlobalScope,
111 load_data: &mut LoadData,
112 element: Option<&Element>,
113 ) -> bool {
114 let Some(csp_list) = self else {
115 return false;
116 };
117 let mut request = Request {
118 url: load_data.url.clone().into_url(),
119 origin: match &load_data.load_origin {
120 LoadOrigin::Script(origin) => origin.immutable().clone().into_url_origin(),
121 _ => Origin::new_opaque(),
122 },
123 redirect_count: 0,
125 destination: Destination::None,
126 initiator: Initiator::None,
127 nonce: "".to_owned(),
128 integrity_metadata: "".to_owned(),
129 parser_metadata: ParserMetadata::None,
130 };
131 let (result, violations) = csp_list.should_navigation_request_be_blocked(
133 &mut request,
134 NavigationCheckType::Other,
135 |script_source| {
136 TrustedScript::get_trusted_type_compliant_string(
139 cx,
140 global,
141 TrustedScriptOrString::String(script_source.into()),
142 "Location href",
143 )
144 .ok()
145 .map(|s| s.into())
146 },
147 );
148
149 load_data.url = request.url.into();
151
152 global.report_csp_violations(violations, element, None);
153
154 result == CheckResult::Blocked
155 }
156
157 fn should_elements_inline_type_behavior_be_blocked(
159 &self,
160 global: &GlobalScope,
161 el: &Element,
162 type_: InlineCheckType,
163 source: &str,
164 current_line: u32,
165 ) -> bool {
166 let Some(csp_list) = self else {
167 return false;
168 };
169 let element = CspElement {
170 nonce: if el.is_nonceable() {
171 Some(Cow::Owned(el.nonce_value().trim().to_owned()))
172 } else {
173 None
174 },
175 };
176 let (result, violations) =
177 csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
178
179 let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
180
181 global.report_csp_violations(violations, Some(el), Some(source_position));
182
183 result == CheckResult::Blocked
184 }
185
186 fn is_trusted_type_policy_creation_allowed(
188 &self,
189 global: &GlobalScope,
190 policy_name: &str,
191 created_policy_names: &[&str],
192 ) -> bool {
193 let Some(csp_list) = self else {
194 return true;
195 };
196
197 let (allowed_by_csp, violations) =
198 csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
199
200 global.report_csp_violations(violations, None, None);
201
202 allowed_by_csp == CheckResult::Allowed
203 }
204
205 fn does_sink_type_require_trusted_types(
207 &self,
208 sink_group: &str,
209 include_report_only_policies: bool,
210 ) -> bool {
211 let Some(csp_list) = self else {
212 return false;
213 };
214
215 csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
216 }
217
218 fn should_sink_type_mismatch_violation_be_blocked_by_csp(
220 &self,
221 global: &GlobalScope,
222 sink: &str,
223 sink_group: &str,
224 source: &str,
225 ) -> bool {
226 let Some(csp_list) = self else {
227 return false;
228 };
229
230 let (allowed_by_csp, violations) = csp_list
231 .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
232
233 global.report_csp_violations(violations, None, None);
234
235 allowed_by_csp == CheckResult::Blocked
236 }
237
238 fn is_base_allowed_for_document(
240 &self,
241 global: &GlobalScope,
242 base: &url::Url,
243 self_origin: &url::Origin,
244 ) -> bool {
245 let Some(csp_list) = self else {
246 return true;
247 };
248
249 let (is_base_allowed, violations) =
250 csp_list.is_base_allowed_for_document(base, self_origin);
251
252 global.report_csp_violations(violations, None, None);
253
254 is_base_allowed == CheckResult::Allowed
255 }
256
257 fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
258 let Some(new_csp_list) = new_csp_list else {
259 return self;
260 };
261
262 match self {
263 None => Some(new_csp_list),
264 Some(mut old_csp_list) => {
265 old_csp_list.append(new_csp_list);
266 Some(old_csp_list)
267 },
268 }
269 }
270}
271
272pub(crate) struct SourcePosition {
273 pub(crate) source_file: String,
274 pub(crate) line_number: u32,
275 pub(crate) column_number: u32,
276}
277
278pub(crate) trait GlobalCspReporting {
279 fn report_csp_violations(
280 &self,
281 violations: Vec<Violation>,
282 element: Option<&Element>,
283 source_position: Option<SourcePosition>,
284 );
285}
286
287#[expect(unsafe_code)]
288fn compute_scripted_caller_source_position() -> SourcePosition {
289 match unsafe { describe_scripted_caller(*GlobalScope::get_cx()) } {
290 Ok(scripted_caller) => SourcePosition {
291 source_file: scripted_caller.filename,
292 line_number: scripted_caller.line,
293 column_number: scripted_caller.col + 1,
294 },
295 Err(()) => SourcePosition {
296 source_file: String::new(),
297 line_number: 0,
298 column_number: 0,
299 },
300 }
301}
302
303fn obtain_blocked_uri_for_violation_resource_with_sample(
305 resource: ViolationResource,
306) -> (Option<String>, String) {
307 match resource {
313 ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
314 ViolationResource::Url(url) => (
316 Some(String::new()),
317 ReportingObserver::strip_url_for_reports(url.into()),
318 ),
319 ViolationResource::TrustedTypePolicy { sample } => {
320 (Some(sample), "trusted-types-policy".to_owned())
321 },
322 ViolationResource::TrustedTypeSink { sample } => {
323 (Some(sample), "trusted-types-sink".to_owned())
324 },
325 ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
326 ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
327 }
328}
329
330impl GlobalCspReporting for GlobalScope {
331 fn report_csp_violations(
333 &self,
334 violations: Vec<Violation>,
335 element: Option<&Element>,
336 source_position: Option<SourcePosition>,
337 ) {
338 if violations.is_empty() {
339 return;
340 }
341 warn!("Reporting CSP violations: {:?}", violations);
342 let source_position =
343 source_position.unwrap_or_else(compute_scripted_caller_source_position);
344 for violation in violations {
345 let (sample, resource) =
346 obtain_blocked_uri_for_violation_resource_with_sample(violation.resource);
347 let report = CSPViolationReportBuilder::default()
348 .resource(resource)
349 .sample(sample)
350 .effective_directive(violation.directive.name)
351 .original_policy(violation.policy.to_string())
352 .report_only(violation.policy.disposition == PolicyDisposition::Report)
353 .source_file(source_position.source_file.clone())
354 .line_number(source_position.line_number)
355 .column_number(source_position.column_number)
356 .build(self);
357 let target = element.and_then(|event_target| {
361 if let Some(window) = self.downcast::<Window>() {
364 if event_target.upcast::<Node>().owner_document() != window.Document() {
368 return None;
369 }
370 }
371 Some(event_target)
372 });
373 let target = match target {
374 None => {
376 if let Some(window) = self.downcast::<Window>() {
378 Trusted::new(window.Document().upcast())
379 } else {
380 Trusted::new(self.upcast())
382 }
383 },
384 Some(event_target) => Trusted::new(event_target.upcast()),
385 };
386 let task =
388 CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
389 self.task_manager()
390 .dom_manipulation_task_source()
391 .queue(task);
392 }
393 }
394}
395
396fn parse_and_potentially_append_to_csp_list(
397 old_csp_list: Option<CspList>,
398 csp_header_iter: ValueIter<HeaderValue>,
399 disposition: PolicyDisposition,
400) -> Option<CspList> {
401 let mut csp_list = old_csp_list;
402 for header in csp_header_iter {
403 let new_csp_list = header
406 .to_str()
407 .ok()
408 .map(|value| CspList::parse(value, PolicySource::Header, disposition));
409 csp_list = csp_list.concatenate(new_csp_list);
410 }
411 csp_list
412}
413
414pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
416 let headers = headers.as_ref()?;
417 let csp_enforce_list = parse_and_potentially_append_to_csp_list(
418 None,
419 headers.get_all("content-security-policy").iter(),
420 PolicyDisposition::Enforce,
421 );
422
423 parse_and_potentially_append_to_csp_list(
424 csp_enforce_list,
425 headers
426 .get_all("content-security-policy-report-only")
427 .iter(),
428 PolicyDisposition::Report,
429 )
430}