Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion moli-csp/src/reporting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ pub fn content_security_policy_reporting_api_report_body(
.to_string()
}

pub(crate) fn csp_url_for_report(url: &Url) -> String {
/// Strip a protected document URL for inclusion in a CSP violation report.
pub fn csp_url_for_report(url: &Url) -> String {
if !matches!(url.scheme(), "http" | "https" | "ws" | "wss") {
return url.scheme().to_owned();
}
Expand Down
3 changes: 3 additions & 0 deletions moli-renderer-v8/src/document_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,9 @@ pub(crate) struct DocumentPolicyContainer {
pub(crate) document_isolation_policy: crate::cross_origin_isolation::DocumentIsolationPolicy,
pub(crate) cross_origin_isolated: bool,
pub(crate) document_content_security_policies: Vec<ContentSecurityPolicy>,
/// Captured with the policy container; local documents inherit this value
/// independently of their base URL and sandboxed Window origin.
pub(crate) content_security_policy_self_url: Option<Url>,
pub(crate) response_content_security_policies: Vec<ContentSecurityPolicy>,
pub(crate) response_content_security_report_only_policies: Vec<ContentSecurityPolicy>,
pub(crate) content_security_reporting_endpoints:
Expand Down
73 changes: 67 additions & 6 deletions moli-renderer-v8/src/document_runtime/security_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ impl DocumentPolicyContainer {
final_url, headers,
),
document_content_security_policies: response_content_security_policies.clone(),
content_security_policy_self_url: Some(final_url.clone()),
sandbox,
response_content_security_policies,
response_content_security_report_only_policies:
Expand Down Expand Up @@ -828,6 +829,7 @@ impl DocumentRuntime {
&self.policy_container,
request_url,
kind,
ContentSecurityPolicyRedirectStatus::NoRedirect,
)
}

Expand All @@ -838,29 +840,42 @@ impl DocumentRuntime {
policy_container: &DocumentPolicyContainer,
request_url: &Url,
kind: DocumentSubresourceCspKind,
redirect_status: ContentSecurityPolicyRedirectStatus,
) -> DocumentContentSecurityPolicyCheck {
let policy_url = policy_container
.content_security_policy_self_url
.as_ref()
.unwrap_or(document_url);
let policies = self.document_content_security_policies_for_optional_document(
document_handle,
&policy_container.response_content_security_policies,
&policy_container.content_security_reporting_endpoints,
);
let enforced_violation = document_url_policy_violation_from_document_policies(
let mut enforced_violation = document_url_policy_violation_from_document_policies(
policies,
document_url,
policy_url,
request_url,
kind.resource_kind(),
ContentSecurityPolicyRedirectStatus::NoRedirect,
redirect_status,
ContentSecurityPolicyDisposition::Enforce,
);
let report_only_violation = document_url_policy_violation(
let mut report_only_violation = document_url_policy_violation(
&policy_container.response_content_security_report_only_policies,
&policy_container.content_security_reporting_endpoints,
document_url,
policy_url,
request_url,
kind.resource_kind(),
ContentSecurityPolicyRedirectStatus::NoRedirect,
redirect_status,
ContentSecurityPolicyDisposition::Report,
);
let document_uri = crate::content_security_policy::csp_url_for_report(document_url);
for violation in [&mut report_only_violation, &mut enforced_violation]
.into_iter()
.flatten()
{
violation.document_uri.clone_from(&document_uri);
violation.source_file.clone_from(&document_uri);
}
DocumentContentSecurityPolicyCheck {
report_only_violation,
enforced_violation,
Expand Down Expand Up @@ -2371,6 +2386,51 @@ mod tests {
)
}

#[test]
fn image_csp_inherited_source_is_independent_of_local_document_and_report_url() {
let runtime = runtime_for_html("<!doctype html>");
let creator = Url::parse("https://creator.example.test/page").unwrap();
let local_document = Url::parse("about:srcdoc").unwrap();
let inherited = DocumentPolicyContainer::from_navigation_response_headers(
&[
("Content-Security-Policy".into(), b"img-src 'self'".to_vec()),
(
"Content-Security-Policy-Report-Only".into(),
b"img-src 'self'".to_vec(),
),
],
&creator,
);
assert_eq!(
inherited.content_security_policy_self_url.as_ref(),
Some(&creator)
);
let allowed = runtime.document_subresource_csp_check_for_document(
None,
&local_document,
&inherited,
&creator.join("image.png").unwrap(),
DocumentSubresourceCspKind::Image,
ContentSecurityPolicyRedirectStatus::NoRedirect,
);
assert!(allowed.has_no_violations());
let (report_only, enforced) = runtime
.document_subresource_csp_check_for_document(
None,
&local_document,
&inherited,
&Url::parse("https://other.example.test/image.png").unwrap(),
DocumentSubresourceCspKind::Image,
ContentSecurityPolicyRedirectStatus::FollowedRedirect,
)
.into_violations();
for violation in [report_only.unwrap(), enforced.unwrap()] {
assert_eq!(violation.document_uri, "about");
assert_eq!(violation.source_file, "about");
assert_eq!(violation.effective_directive, "img-src");
}
}

#[test]
fn csp_script_src_self_blocks_cross_origin_script_element_request() {
let runtime = runtime_for_html(
Expand Down Expand Up @@ -2542,6 +2602,7 @@ mod tests {
&policy,
&request_url,
DocumentSubresourceCspKind::Image,
ContentSecurityPolicyRedirectStatus::NoRedirect,
);
let violation = check
.enforced_violation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ impl JsContextHost {
.map(|entry| entry.document_policy_container_snapshot())
})
.unwrap_or_else(|| self.document_policy_container().clone());
policy_container
.content_security_policy_self_url
.get_or_insert_with(|| self.document_url_for_child_context(handle));
if let Some(parent_document) = self
.dom_host()
.node(handle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ impl JsContextHost {
handle: DomHandle,
url: &Url,
) -> Option<ChildBrowsingContextSnapshot> {
let snapshot = self.apply_page_csp_bypass_to_child_snapshot(
let mut snapshot = self.apply_page_csp_bypass_to_child_snapshot(
self.materialize_local_child_snapshot_for_url(url)?,
);
if moli_url::is_about_blank(&snapshot.url) {
snapshot.policy_container.content_security_policy_self_url = self
.initial_child_about_blank_policy_container_from_parent(handle)
.content_security_policy_self_url;
Some(snapshot.with_fallback_base_url(self.document_base_url_for_child_context(handle)))
} else {
Some(snapshot)
Expand Down Expand Up @@ -102,13 +105,17 @@ impl JsContextHost {
),
)
}
ChildBrowsingContextBootstrap::Srcdoc { base_url, markup } => Some(
self.apply_page_csp_bypass_to_child_snapshot(ChildBrowsingContextSnapshot::srcdoc(
ChildBrowsingContextBootstrap::Srcdoc { base_url, markup } => {
let mut snapshot = ChildBrowsingContextSnapshot::srcdoc(
base_url.clone(),
markup.clone(),
self.document_character_set().to_owned(),
)),
),
);
snapshot.policy_container.content_security_policy_self_url = self
.initial_child_about_blank_policy_container_from_parent(handle)
.content_security_policy_self_url;
Some(self.apply_page_csp_bypass_to_child_snapshot(snapshot))
}
ChildBrowsingContextBootstrap::Url(url) => {
self.materialize_local_child_snapshot_for_navigation_url(handle, url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ impl ChildBrowsingContextEntry {
credentialless: bool,
credentialless_storage_nonce: Option<moli_storage_key::OpaqueOriginNonce>,
) {
self.document_policy_container
.content_security_policy_self_url =
policy_container.content_security_policy_self_url.clone();
self.document_policy_container.referrer_policy = policy_container.referrer_policy.clone();
self.document_policy_container.cross_origin_embedder_policy =
policy_container.cross_origin_embedder_policy;
Expand Down Expand Up @@ -366,6 +369,11 @@ impl ChildBrowsingContextEntry {
&mut self,
snapshot: &ChildBrowsingContextSnapshot,
) {
self.document_policy_container
.content_security_policy_self_url = snapshot
.policy_container
.content_security_policy_self_url
.clone();
let sandbox = self
.document_policy_container
.sandbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ impl JsContextHost {
.unwrap_or_else(|| creator_document_url.to_string())
};
let document_policy_container = ChildDocumentPolicyContainer {
content_security_policy_self_url: refresh_policy_source
.and_then(|policy| policy.content_security_policy_self_url.clone()),
document_referrer,
referrer_policy: refresh_policy_source
.and_then(|policy| policy.referrer_policy.clone()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl CssImageResourceStore {
return false;
};
let descriptor = match &slot.state {
CssImageResourceState::DecodeQueued(descriptor) => *descriptor,
CssImageResourceState::DecodeQueued(descriptor) => descriptor.clone(),
_ => return false,
};
let (pixels, svg) = match ready.content {
Expand Down Expand Up @@ -381,7 +381,7 @@ mod tests {
else {
panic!("the first exact request must fetch");
};
assert!(store.mark_decode_queued(&first, descriptor));
assert!(store.mark_decode_queued(&first, descriptor.clone()));

let CssImageResourceAdmission::Fetch(replacement) =
store.admit(first_document, owner(2), url.to_owned())
Expand All @@ -392,7 +392,7 @@ mod tests {
!store.complete_decode(&first, ready_red_pixel()),
"the retired owner cannot commit into the replacement slot"
);
assert!(store.mark_decode_queued(&replacement, descriptor));
assert!(store.mark_decode_queued(&replacement, descriptor.clone()));
assert!(store.complete_decode(&replacement, ready_red_pixel()));
assert!(
!store.fail(&replacement),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ pub(super) struct ImageResourceRequestIdentity {
pub(super) request_key: ImageRequestKey,
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ImageResponseDescriptor {
pub(crate) width: u32,
pub(crate) height: u32,
pub(super) decode_metadata: ImageDecodeMetadata,
pub(crate) response_url: Option<std::sync::Arc<url::Url>>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
Expand All @@ -55,6 +56,7 @@ impl ImageResponseDescriptor {
width: metadata.width,
height: metadata.height,
decode_metadata: ImageDecodeMetadata::Raster(metadata),
response_url: None,
}
}

Expand All @@ -64,6 +66,7 @@ impl ImageResponseDescriptor {
width,
height,
decode_metadata: ImageDecodeMetadata::Svg(metadata),
response_url: None,
})
}
}
Expand Down Expand Up @@ -260,7 +263,7 @@ impl super::JsContextHost {
if !self
.image_resources
.css
.mark_decode_queued(identity, descriptor)
.mark_decode_queued(identity, descriptor.clone())
{
return false;
}
Expand Down Expand Up @@ -307,6 +310,39 @@ impl super::JsContextHost {
self.image_resources.is_ready(element)
}

pub(crate) fn cached_image_response_url(
&self,
element: DomHandle,
) -> Option<std::sync::Arc<url::Url>> {
let identity = self.image_resources.identity(element)?;
self.image_resources
.ready_resource(&identity.request_key)?
.descriptor
.response_url
.clone()
.filter(|url| url.as_str() != identity.request_key.url())
}

pub(crate) fn cached_css_image_response_url(
&self,
request_url: &url::Url,
) -> Option<std::sync::Arc<url::Url>> {
let key = ImageRequestKey::with_density(
request_url.to_string(),
crate::types::ImageRequestCorsMode::NoCors,
1.0,
);
let resource = self.image_resources.ready_resource(&key)?;
if resource.pixels.is_none() && resource.svg.is_none() {
return None;
}
resource
.descriptor
.response_url
.clone()
.filter(|url| url.as_ref() != request_url)
}

pub(crate) fn has_ready_image_request(&self, request_key: &ImageRequestKey) -> bool {
self.image_resources.has_ready_request(request_key)
}
Expand Down Expand Up @@ -422,7 +458,11 @@ impl super::JsContextHost {
.request_client()
.optional_resource_fetch_enabled(crate::types::SubresourceResourceType::Image)
});
if decode_enabled && self.image_resources.complete_shared_ready(&identity) {
if decode_enabled
&& self
.image_resources
.complete_shared_ready(&identity, descriptor.response_url.as_deref())
{
let Some(pending) = self.pending_image_load_events.get_mut(&element) else {
return ImageResponseCompletion::Ignored;
};
Expand All @@ -434,7 +474,7 @@ impl super::JsContextHost {
if decode_enabled {
if !self
.image_resources
.mark_decode_queued(&identity, descriptor)
.mark_decode_queued(&identity, descriptor.clone())
{
return ImageResponseCompletion::Ignored;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ mod tests {
moli_image::RgbaImage::try_new(1, 1, vec![1, 2, 3, 255]).expect("valid image pixels");
let encoded = moli_image::encode_png(&pixels).expect("valid PNG");
let response = crate::protocol_types::NavigationResponse::from_text_body(
url,
url.clone(),
200,
vec![("Content-Type".to_owned(), b"image/png".to_vec())],
String::new(),
Expand Down Expand Up @@ -598,7 +598,11 @@ mod tests {
drop(claimed);
drop(outcome);
drop(load);
assert!(resources.complete_shared_ready(&identity));
assert!(!resources.complete_shared_ready(
&identity,
Some(&url::Url::parse("https://other.test/image.png").unwrap()),
));
assert!(resources.complete_shared_ready(&identity, Some(&url)));
let ready = resources
.ready_for_layout(element)
.expect("the exact element should bind the decoded preload");
Expand Down
Loading
Loading