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 crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
22use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::refcounted::Trusted;
25use crate::dom::csppolicyviolationreport::CSPViolationReportBuilder;
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 concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList>;
71}
72
73impl CspReporting for Option<CspList> {
74 fn is_js_evaluation_allowed(&self, global: &GlobalScope, source: &str) -> bool {
76 let Some(csp_list) = self else {
77 return true;
78 };
79
80 let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source);
81
82 global.report_csp_violations(violations, None, None);
83
84 is_js_evaluation_allowed == CheckResult::Allowed
85 }
86
87 fn is_wasm_evaluation_allowed(&self, global: &GlobalScope) -> bool {
89 let Some(csp_list) = self else {
90 return true;
91 };
92
93 let (is_wasm_evaluation_allowed, violations) = csp_list.is_wasm_evaluation_allowed();
94
95 global.report_csp_violations(violations, None, None);
96
97 is_wasm_evaluation_allowed == CheckResult::Allowed
98 }
99
100 fn should_navigation_request_be_blocked(
102 &self,
103 global: &GlobalScope,
104 load_data: &mut LoadData,
105 element: Option<&Element>,
106 can_gc: CanGc,
107 ) -> bool {
108 let Some(csp_list) = self else {
109 return false;
110 };
111 let mut request = Request {
112 url: load_data.url.clone().into_url(),
113 origin: match &load_data.load_origin {
114 LoadOrigin::Script(immutable_origin) => immutable_origin.clone().into_url_origin(),
115 _ => Origin::new_opaque(),
116 },
117 redirect_count: 0,
119 destination: Destination::None,
120 initiator: Initiator::None,
121 nonce: "".to_owned(),
122 integrity_metadata: "".to_owned(),
123 parser_metadata: ParserMetadata::None,
124 };
125 let (result, violations) = csp_list.should_navigation_request_be_blocked(
127 &mut request,
128 NavigationCheckType::Other,
129 |script_source| {
130 TrustedScript::get_trusted_script_compliant_string(
133 global,
134 TrustedScriptOrString::String(script_source.into()),
135 "Location href",
136 can_gc,
137 )
138 .ok()
139 .map(|s| s.into())
140 },
141 );
142
143 load_data.url = request.url.into();
145
146 global.report_csp_violations(violations, element, None);
147
148 result == CheckResult::Blocked
149 }
150
151 fn should_elements_inline_type_behavior_be_blocked(
153 &self,
154 global: &GlobalScope,
155 el: &Element,
156 type_: InlineCheckType,
157 source: &str,
158 current_line: u32,
159 ) -> bool {
160 let Some(csp_list) = self else {
161 return false;
162 };
163 let element = CspElement {
164 nonce: el.nonce_value_if_nonceable().map(Cow::Owned),
165 };
166 let (result, violations) =
167 csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source);
168
169 let source_position = el.compute_source_position(current_line.saturating_sub(2).max(1));
170
171 global.report_csp_violations(violations, Some(el), Some(source_position));
172
173 result == CheckResult::Blocked
174 }
175
176 fn is_trusted_type_policy_creation_allowed(
178 &self,
179 global: &GlobalScope,
180 policy_name: &str,
181 created_policy_names: &[&str],
182 ) -> bool {
183 let Some(csp_list) = self else {
184 return true;
185 };
186
187 let (allowed_by_csp, violations) =
188 csp_list.is_trusted_type_policy_creation_allowed(policy_name, created_policy_names);
189
190 global.report_csp_violations(violations, None, None);
191
192 allowed_by_csp == CheckResult::Allowed
193 }
194
195 fn does_sink_type_require_trusted_types(
197 &self,
198 sink_group: &str,
199 include_report_only_policies: bool,
200 ) -> bool {
201 let Some(csp_list) = self else {
202 return false;
203 };
204
205 csp_list.does_sink_type_require_trusted_types(sink_group, include_report_only_policies)
206 }
207
208 fn should_sink_type_mismatch_violation_be_blocked_by_csp(
210 &self,
211 global: &GlobalScope,
212 sink: &str,
213 sink_group: &str,
214 source: &str,
215 ) -> bool {
216 let Some(csp_list) = self else {
217 return false;
218 };
219
220 let (allowed_by_csp, violations) = csp_list
221 .should_sink_type_mismatch_violation_be_blocked_by_csp(sink, sink_group, source);
222
223 global.report_csp_violations(violations, None, None);
224
225 allowed_by_csp == CheckResult::Blocked
226 }
227
228 fn concatenate(self, new_csp_list: Option<CspList>) -> Option<CspList> {
229 let Some(new_csp_list) = new_csp_list else {
230 return self;
231 };
232
233 match self {
234 None => Some(new_csp_list),
235 Some(mut old_csp_list) => {
236 old_csp_list.append(new_csp_list);
237 Some(old_csp_list)
238 },
239 }
240 }
241}
242
243pub(crate) struct SourcePosition {
244 pub(crate) source_file: String,
245 pub(crate) line_number: u32,
246 pub(crate) column_number: u32,
247}
248
249pub(crate) trait GlobalCspReporting {
250 fn report_csp_violations(
251 &self,
252 violations: Vec<Violation>,
253 element: Option<&Element>,
254 source_position: Option<SourcePosition>,
255 );
256}
257
258#[expect(unsafe_code)]
259fn compute_scripted_caller_source_position() -> SourcePosition {
260 let scripted_caller =
261 unsafe { describe_scripted_caller(*GlobalScope::get_cx()) }.unwrap_or_default();
262
263 SourcePosition {
264 source_file: scripted_caller.filename,
265 line_number: scripted_caller.line,
266 column_number: scripted_caller.col + 1,
267 }
268}
269
270impl GlobalCspReporting for GlobalScope {
271 fn report_csp_violations(
273 &self,
274 violations: Vec<Violation>,
275 element: Option<&Element>,
276 source_position: Option<SourcePosition>,
277 ) {
278 if violations.is_empty() {
279 return;
280 }
281 warn!("Reporting CSP violations: {:?}", violations);
282 let source_position =
283 source_position.unwrap_or_else(compute_scripted_caller_source_position);
284 for violation in violations {
285 let (sample, resource) = match violation.resource {
286 ViolationResource::Inline { sample } => (sample, "inline".to_owned()),
287 ViolationResource::Url(url) => (Some(String::new()), url.into()),
288 ViolationResource::TrustedTypePolicy { sample } => {
289 (Some(sample), "trusted-types-policy".to_owned())
290 },
291 ViolationResource::TrustedTypeSink { sample } => {
292 (Some(sample), "trusted-types-sink".to_owned())
293 },
294 ViolationResource::Eval { sample } => (sample, "eval".to_owned()),
295 ViolationResource::WasmEval => (None, "wasm-eval".to_owned()),
296 };
297 let report = CSPViolationReportBuilder::default()
298 .resource(resource)
299 .sample(sample)
300 .effective_directive(violation.directive.name)
301 .original_policy(violation.policy.to_string())
302 .report_only(violation.policy.disposition == PolicyDisposition::Report)
303 .source_file(source_position.source_file.clone())
304 .line_number(source_position.line_number)
305 .column_number(source_position.column_number)
306 .build(self);
307 let target = element.and_then(|event_target| {
311 if let Some(window) = self.downcast::<Window>() {
314 if event_target.upcast::<Node>().owner_document() != window.Document() {
318 return None;
319 }
320 }
321 Some(event_target)
322 });
323 let target = match target {
324 None => {
326 if let Some(window) = self.downcast::<Window>() {
328 Trusted::new(window.Document().upcast())
329 } else {
330 Trusted::new(self.upcast())
332 }
333 },
334 Some(event_target) => Trusted::new(event_target.upcast()),
335 };
336 let task =
338 CSPViolationReportTask::new(Trusted::new(self), target, report, violation.policy);
339 self.task_manager()
340 .dom_manipulation_task_source()
341 .queue(task);
342 }
343 }
344}
345
346fn parse_and_potentially_append_to_csp_list(
347 old_csp_list: Option<CspList>,
348 csp_header_iter: ValueIter<HeaderValue>,
349 disposition: PolicyDisposition,
350) -> Option<CspList> {
351 let mut csp_list = old_csp_list;
352 for header in csp_header_iter {
353 let new_csp_list = header
356 .to_str()
357 .ok()
358 .map(|value| CspList::parse(value, PolicySource::Header, disposition));
359 csp_list = csp_list.concatenate(new_csp_list);
360 }
361 csp_list
362}
363
364pub(crate) fn parse_csp_list_from_metadata(headers: &Option<Serde<HeaderMap>>) -> Option<CspList> {
366 let headers = headers.as_ref()?;
367 let csp_enforce_list = parse_and_potentially_append_to_csp_list(
368 None,
369 headers.get_all("content-security-policy").iter(),
370 PolicyDisposition::Enforce,
371 );
372
373 parse_and_potentially_append_to_csp_list(
374 csp_enforce_list,
375 headers
376 .get_all("content-security-policy-report-only")
377 .iter(),
378 PolicyDisposition::Report,
379 )
380}