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::trustedscript::TrustedScript;
30use crate::dom::window::Window;
31use crate::script_runtime::CanGc;
32use crate::security_manager::CSPViolationReportTask;
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 global: &GlobalScope,
40 load_data: &mut LoadData,
41 element: Option<&Element>,
42 can_gc: CanGc,
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 global: &GlobalScope,
110 load_data: &mut LoadData,
111 element: Option<&Element>,
112 can_gc: CanGc,
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_script_compliant_string(
139 global,
140 TrustedScriptOrString::String(script_source.into()),
141 "Location href",
142 can_gc,
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: el.nonce_value_if_nonceable().map(Cow::Owned),
171 };
172 let (result, violations) =
173 csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
174
175 let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
176
177 global.report_csp_violations(violations, Some(el), Some(source_position));
178
179 result == CheckResult::Blocked
180 }
181
182 fn is_trusted_type_policy_creation_allowed(
184 &self,
185 global: &GlobalScope,
186 policy_name: &str,
187 created_policy_names: &[&str],
188 ) -> bool {
189 let Some(csp_list) = self else {
190 return true;
191 };
192
193 let (allowed_by_csp, violations) =
194 csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
195
196 global.report_csp_violations(violations, None, None);
197
198 allowed_by_csp == CheckResult::Allowed
199 }
200
201 fn does_sink_type_require_trusted_types(
203 &self,
204 sink_group: &str,
205 include_report_only_policies: bool,
206 ) -> bool {
207 let Some(csp_list) = self else {
208 return false;
209 };
210
211 csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
212 }
213
214 fn should_sink_type_mismatch_violation_be_blocked_by_csp(
216 &self,
217 global: &GlobalScope,
218 sink: &str,
219 sink_group: &str,
220 source: &str,
221 ) -> bool {
222 let Some(csp_list) = self else {
223 return false;
224 };
225
226 let (allowed_by_csp, violations) = csp_list
227 .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
228
229 global.report_csp_violations(violations, None, None);
230
231 allowed_by_csp == CheckResult::Blocked
232 }
233
234 fn is_base_allowed_for_document(
236 &self,
237 global: &GlobalScope,
238 base: &url::Url,
239 self_origin: &url::Origin,
240 ) -> bool {
241 let Some(csp_list) = self else {
242 return true;
243 };
244
245 let (is_base_allowed, violations) =
246 csp_list.is_base_allowed_for_document(base, self_origin);
247
248 global.report_csp_violations(violations, None, None);
249
250 is_base_allowed == CheckResult::Allowed
251 }
252
253 fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
254 let Some(new_csp_list) = new_csp_list else {
255 return self;
256 };
257
258 match self {
259 None => Some(new_csp_list),
260 Some(mut old_csp_list) => {
261 old_csp_list.append(new_csp_list);
262 Some(old_csp_list)
263 },
264 }
265 }
266}
267
268pub(crate) struct SourcePosition {
269 pub(crate) source_file: String,
270 pub(crate) line_number: u32,
271 pub(crate) column_number: u32,
272}
273
274pub(crate) trait GlobalCspReporting {
275 fn report_csp_violations(
276 &self,
277 violations: Vec<Violation>,
278 element: Option<&Element>,
279 source_position: Option<SourcePosition>,
280 );
281}
282
283#[expect(unsafe_code)]
284fn compute_scripted_caller_source_position() -> SourcePosition {
285 let scripted_caller =
286 unsafe { describe_scripted_caller(*GlobalScope::get_cx()) }.unwrap_or_default();
287
288 SourcePosition {
289 source_file: scripted_caller.filename,
290 line_number: scripted_caller.line,
291 column_number: scripted_caller.col + 1,
292 }
293}
294
295impl GlobalCspReporting for GlobalScope {
296 fn report_csp_violations(
298 &self,
299 violations: Vec<Violation>,
300 element: Option<&Element>,
301 source_position: Option<SourcePosition>,
302 ) {
303 if violations.is_empty() {
304 return;
305 }
306 warn!("Reporting CSP violations: {:?}", violations);
307 let source_position =
308 source_position.unwrap_or_else(compute_scripted_caller_source_position);
309 for violation in violations {
310 let (sample, resource) = match violation.resource {
311 ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
312 ViolationResource::Url(url) => (Some(String::new()), url.into()),
313 ViolationResource::TrustedTypePolicy { sample } => {
314 (Some(sample), "trusted-types-policy".to_owned())
315 },
316 ViolationResource::TrustedTypeSink { sample } => {
317 (Some(sample), "trusted-types-sink".to_owned())
318 },
319 ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
320 ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
321 };
322 let report = CSPViolationReportBuilder::default()
323 .resource(resource)
324 .sample(sample)
325 .effective_directive(violation.directive.name)
326 .original_policy(violation.policy.to_string())
327 .report_only(violation.policy.disposition == PolicyDisposition::Report)
328 .source_file(source_position.source_file.clone())
329 .line_number(source_position.line_number)
330 .column_number(source_position.column_number)
331 .build(self);
332 let target = element.and_then(|event_target| {
336 if let Some(window) = self.downcast::<Window>() {
339 if event_target.upcast::<Node>().owner_document() != window.Document() {
343 return None;
344 }
345 }
346 Some(event_target)
347 });
348 let target = match target {
349 None => {
351 if let Some(window) = self.downcast::<Window>() {
353 Trusted::new(window.Document().upcast())
354 } else {
355 Trusted::new(self.upcast())
357 }
358 },
359 Some(event_target) => Trusted::new(event_target.upcast()),
360 };
361 let task =
363 CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
364 self.task_manager()
365 .dom_manipulation_task_source()
366 .queue(task);
367 }
368 }
369}
370
371fn parse_and_potentially_append_to_csp_list(
372 old_csp_list: Option<CspList>,
373 csp_header_iter: ValueIter<HeaderValue>,
374 disposition: PolicyDisposition,
375) -> Option<CspList> {
376 let mut csp_list = old_csp_list;
377 for header in csp_header_iter {
378 let new_csp_list = header
381 .to_str()
382 .ok()
383 .map(|value| CspList::parse(value, PolicySource::Header, disposition));
384 csp_list = csp_list.concatenate(new_csp_list);
385 }
386 csp_list
387}
388
389pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
391 let headers = headers.as_ref()?;
392 let csp_enforce_list = parse_and_potentially_append_to_csp_list(
393 None,
394 headers.get_all("content-security-policy").iter(),
395 PolicyDisposition::Enforce,
396 );
397
398 parse_and_potentially_append_to_csp_list(
399 csp_enforce_list,
400 headers
401 .get_all("content-security-policy-report-only")
402 .iter(),
403 PolicyDisposition::Report,
404 )
405}