diff options
-rw-r--r-- | Cargo.lock | 18 | ||||
-rw-r--r-- | components/malloc_size_of/Cargo.toml | 2 | ||||
-rw-r--r-- | components/malloc_size_of/lib.rs | 6 | ||||
-rw-r--r-- | components/net/Cargo.toml | 1 | ||||
-rw-r--r-- | components/net/fetch/methods.rs | 37 | ||||
-rw-r--r-- | components/net_traits/Cargo.toml | 1 | ||||
-rw-r--r-- | components/net_traits/request.rs | 46 | ||||
-rw-r--r-- | components/script/Cargo.toml | 1 | ||||
-rw-r--r-- | components/script/dom/bindings/trace.rs | 3 | ||||
-rw-r--r-- | components/script/dom/document.rs | 40 | ||||
-rw-r--r-- | components/script/dom/globalscope.rs | 10 | ||||
-rw-r--r-- | components/script/dom/htmlscriptelement.rs | 12 | ||||
-rw-r--r-- | components/script/dom/request.rs | 4 | ||||
-rw-r--r-- | components/script/dom/servoparser/mod.rs | 28 | ||||
-rw-r--r-- | components/script/fetch.rs | 2 | ||||
-rw-r--r-- | tests/wpt/metadata/fetch/api/policies/csp-blocked.html.ini | 5 |
16 files changed, 175 insertions, 41 deletions
diff --git a/Cargo.lock b/Cargo.lock index 8705e2f7e9b..2cf04a97760 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -737,6 +737,20 @@ dependencies = [ ] [[package]] +name = "content-security-policy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30ee9967a875968e66f6690e299f06781ed109cb82d10e0d60a126a38d61947" +dependencies = [ + "bitflags", + "lazy_static", + "percent-encoding", + "regex", + "serde", + "url", +] + +[[package]] name = "cookie" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2835,6 +2849,7 @@ name = "malloc_size_of" version = "0.0.1" dependencies = [ "app_units", + "content-security-policy", "crossbeam-channel", "cssparser", "euclid", @@ -3156,6 +3171,7 @@ dependencies = [ "base64", "brotli", "bytes", + "content-security-policy", "cookie", "crossbeam-channel", "devtools_traits", @@ -3215,6 +3231,7 @@ dependencies = [ name = "net_traits" version = "0.0.1" dependencies = [ + "content-security-policy", "cookie", "embedder_traits", "headers", @@ -4122,6 +4139,7 @@ dependencies = [ "canvas_traits", "caseless", "chrono", + "content-security-policy", "cookie", "crossbeam-channel", "cssparser", diff --git a/components/malloc_size_of/Cargo.toml b/components/malloc_size_of/Cargo.toml index 713d2d59fef..e6a33d7e7c4 100644 --- a/components/malloc_size_of/Cargo.toml +++ b/components/malloc_size_of/Cargo.toml @@ -21,10 +21,12 @@ servo = [ "url", "webrender_api", "xml5ever", + "content-security-policy", ] [dependencies] app_units = "0.7" +content-security-policy = {version = "0.3.0", features = ["serde"], optional = true} crossbeam-channel = { version = "0.3", optional = true } cssparser = "0.25" euclid = "0.20" diff --git a/components/malloc_size_of/lib.rs b/components/malloc_size_of/lib.rs index d7e0a6fdf16..af7147d8edb 100644 --- a/components/malloc_size_of/lib.rs +++ b/components/malloc_size_of/lib.rs @@ -48,6 +48,8 @@ extern crate app_units; #[cfg(feature = "servo")] +extern crate content_security_policy; +#[cfg(feature = "servo")] extern crate crossbeam_channel; extern crate cssparser; extern crate euclid; @@ -80,6 +82,8 @@ extern crate webrender_api; extern crate xml5ever; #[cfg(feature = "servo")] +use content_security_policy as csp; +#[cfg(feature = "servo")] use serde_bytes::ByteBuf; use std::hash::{BuildHasher, Hash}; use std::mem::size_of; @@ -833,6 +837,8 @@ malloc_size_of_is_0!(app_units::Au); malloc_size_of_is_0!(cssparser::RGBA, cssparser::TokenSerializationType); +malloc_size_of_is_0!(csp::Destination); + #[cfg(feature = "url")] impl MallocSizeOf for url::Host { fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { diff --git a/components/net/Cargo.toml b/components/net/Cargo.toml index a91cc6d8a97..f05d5b31268 100644 --- a/components/net/Cargo.toml +++ b/components/net/Cargo.toml @@ -17,6 +17,7 @@ doctest = false base64 = "0.10.1" brotli = "3" bytes = "0.4" +content-security-policy = {version = "0.3.0", features = ["serde"]} cookie_rs = {package = "cookie", version = "0.11"} crossbeam-channel = "0.3" devtools_traits = {path = "../devtools_traits"} diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index b60265ce2b8..fd225991568 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -8,6 +8,7 @@ use crate::filemanager_thread::{fetch_file_in_chunks, FileManager, FILE_CHUNK_SI use crate::http_loader::{determine_request_referrer, http_fetch, HttpState}; use crate::http_loader::{set_default_accept, set_default_accept_language}; use crate::subresource_integrity::is_response_integrity_valid; +use content_security_policy as csp; use crossbeam_channel::{unbounded, Receiver, Sender}; use devtools_traits::DevtoolsControlMsg; use headers::{AccessControlExposeHeaders, ContentType, HeaderMapExt, Range}; @@ -138,6 +139,30 @@ pub fn fetch_with_cors_cache( main_fetch(request, cache, false, false, target, &mut None, &context); } +/// https://www.w3.org/TR/CSP/#should-block-request +pub fn should_request_be_blocked_by_csp(request: &Request) -> csp::CheckResult { + let origin = match &request.origin { + Origin::Client => return csp::CheckResult::Allowed, + Origin::Origin(origin) => origin, + }; + let csp_request = csp::Request { + url: request.url().into_url(), + origin: origin.clone().into_url_origin(), + redirect_count: request.redirect_count, + destination: request.destination, + initiator: csp::Initiator::None, + nonce: String::new(), + integrity_metadata: request.integrity_metadata.clone(), + parser_metadata: csp::ParserMetadata::None, + }; + // TODO: Instead of ignoring violations, report them. + request + .csp_list + .as_ref() + .map(|c| c.should_request_be_blocked(&csp_request).0) + .unwrap_or(csp::CheckResult::Allowed) +} + /// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) pub fn main_fetch( request: &mut Request, @@ -163,8 +188,18 @@ pub fn main_fetch( } } + // Step 2.2. + // TODO: Report violations. + + // Step 2.4. + if should_request_be_blocked_by_csp(request) == csp::CheckResult::Blocked { + response = Some(Response::network_error(NetworkError::Internal( + "Blocked by Content-Security-Policy".into(), + ))) + } + // Step 3. - // TODO: handle content security policy violations. + // TODO: handle request abort. // Step 4. // TODO: handle upgrade to a potentially secure URL. diff --git a/components/net_traits/Cargo.toml b/components/net_traits/Cargo.toml index 4bda5d8d0cf..caf1f9787fc 100644 --- a/components/net_traits/Cargo.toml +++ b/components/net_traits/Cargo.toml @@ -13,6 +13,7 @@ test = false doctest = false [dependencies] +content-security-policy = {version = "0.3.0", features = ["serde"]} cookie = "0.11" embedder_traits = { path = "../embedder_traits" } headers = "0.2" diff --git a/components/net_traits/request.rs b/components/net_traits/request.rs index dad0ae8ca35..07ccdf60695 100644 --- a/components/net_traits/request.rs +++ b/components/net_traits/request.rs @@ -4,6 +4,7 @@ use crate::ReferrerPolicy; use crate::ResourceTimingType; +use content_security_policy::{self as csp, CspList}; use http::HeaderMap; use hyper::Method; use msg::constellation_msg::PipelineId; @@ -20,37 +21,7 @@ pub enum Initiator { } /// A request [destination](https://fetch.spec.whatwg.org/#concept-request-destination) -#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] -pub enum Destination { - None, - Audio, - Document, - Embed, - Font, - Image, - Manifest, - Object, - Report, - Script, - ServiceWorker, - SharedWorker, - Style, - Track, - Video, - Worker, - Xslt, -} - -impl Destination { - /// https://fetch.spec.whatwg.org/#request-destination-script-like - #[inline] - pub fn is_script_like(&self) -> bool { - *self == Destination::Script || - *self == Destination::ServiceWorker || - *self == Destination::SharedWorker || - *self == Destination::Worker - } -} +pub use csp::Destination; /// A request [origin](https://fetch.spec.whatwg.org/#concept-request-origin) #[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] @@ -175,6 +146,11 @@ pub struct RequestBuilder { pub pipeline_id: Option<PipelineId>, pub redirect_mode: RedirectMode, pub integrity_metadata: String, + // This is nominally a part of the client's global object. + // It is copied here to avoid having to reach across the thread + // boundary every time a redirect occurs. + #[ignore_malloc_size_of = "Defined in rust-content-security-policy"] + pub csp_list: Option<CspList>, // to keep track of redirects pub url_list: Vec<ServoUrl>, pub parser_metadata: ParserMetadata, @@ -206,6 +182,7 @@ impl RequestBuilder { url_list: vec![], parser_metadata: ParserMetadata::Default, initiator: Initiator::None, + csp_list: None, } } @@ -329,6 +306,7 @@ impl RequestBuilder { request.url_list = url_list; request.integrity_metadata = self.integrity_metadata; request.parser_metadata = self.parser_metadata; + request.csp_list = self.csp_list; request } } @@ -396,6 +374,11 @@ pub struct Request { pub response_tainting: ResponseTainting, /// <https://fetch.spec.whatwg.org/#concept-request-parser-metadata> pub parser_metadata: ParserMetadata, + // This is nominally a part of the client's global object. + // It is copied here to avoid having to reach across the thread + // boundary every time a redirect occurs. + #[ignore_malloc_size_of = "Defined in rust-content-security-policy"] + pub csp_list: Option<CspList>, } impl Request { @@ -428,6 +411,7 @@ impl Request { parser_metadata: ParserMetadata::Default, redirect_count: 0, response_tainting: ResponseTainting::Basic, + csp_list: None, } } diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 8cda8242dea..e521235f1c6 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -38,6 +38,7 @@ bitflags = "1.0" bluetooth_traits = {path = "../bluetooth_traits"} canvas_traits = {path = "../canvas_traits"} caseless = "0.2" +content-security-policy = {version = "0.3.0", features = ["serde"]} cookie = "0.11" chrono = "0.4" crossbeam-channel = "0.3" diff --git a/components/script/dom/bindings/trace.rs b/components/script/dom/bindings/trace.rs index e86c96e0a9e..a914b8161a6 100644 --- a/components/script/dom/bindings/trace.rs +++ b/components/script/dom/bindings/trace.rs @@ -54,6 +54,7 @@ use canvas_traits::webgl::{ use canvas_traits::webgl::{WebGLFramebufferId, WebGLMsgSender, WebGLPipeline, WebGLProgramId}; use canvas_traits::webgl::{WebGLReceiver, WebGLRenderbufferId, WebGLSLVersion, WebGLSender}; use canvas_traits::webgl::{WebGLShaderId, WebGLSyncId, WebGLTextureId, WebGLVersion}; +use content_security_policy::CspList; use crossbeam_channel::{Receiver, Sender}; use cssparser::RGBA; use devtools_traits::{CSSError, TimelineMarkerType, WorkerId}; @@ -170,6 +171,8 @@ unsafe_no_jsmanaged_fields!(*mut JobQueue); unsafe_no_jsmanaged_fields!(Cow<'static, str>); +unsafe_no_jsmanaged_fields!(CspList); + /// Trace a `JSVal`. pub fn trace_jsval(tracer: *mut JSTracer, description: &str, val: &Heap<JSVal>) { unsafe { diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 452212a2b3c..6581dbd54bb 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -110,6 +110,7 @@ use crate::task::TaskBox; use crate::task_source::{TaskSource, TaskSourceName}; use crate::timers::OneshotTimerCallback; use canvas_traits::webgl::{self, WebGLContextId, WebGLMsg}; +use content_security_policy::{self as csp, CspList}; use cookie::Cookie; use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; @@ -137,6 +138,7 @@ use num_traits::ToPrimitive; use percent_encoding::percent_decode; use profile_traits::ipc as profile_ipc; use profile_traits::time::{TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType}; +use ref_filter_map::ref_filter_map; use ref_slice::ref_slice; use script_layout_interface::message::{Msg, ReflowGoal}; use script_traits::{AnimationState, DocumentActivity, MouseButton, MouseEventType}; @@ -148,7 +150,7 @@ use servo_atoms::Atom; use servo_config::pref; use servo_media::{ClientContextId, ServoMedia}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; -use std::borrow::ToOwned; +use std::borrow::Cow; use std::cell::{Cell, Ref, RefMut}; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::{HashMap, HashSet, VecDeque}; @@ -398,6 +400,9 @@ pub struct Document { media_controls: DomRefCell<HashMap<String, Dom<ShadowRoot>>>, /// List of all WebGL context IDs that need flushing. dirty_webgl_contexts: DomRefCell<HashSet<WebGLContextId>>, + /// https://html.spec.whatwg.org/multipage/#concept-document-csp-list + #[ignore_malloc_size_of = "Defined in rust-content-security-policy"] + csp_list: DomRefCell<Option<CspList>>, } #[derive(JSTraceable, MallocSizeOf)] @@ -1734,9 +1739,10 @@ impl Document { pub fn fetch_async( &self, load: LoadType, - request: RequestBuilder, + mut request: RequestBuilder, fetch_target: IpcSender<FetchResponseMsg>, ) { + request.csp_list = self.get_csp_list().map(|x| x.clone()); let mut loader = self.loader.borrow_mut(); loader.fetch_async(load, request, fetch_target); } @@ -2806,9 +2812,39 @@ impl Document { shadow_roots_styles_changed: Cell::new(false), media_controls: DomRefCell::new(HashMap::new()), dirty_webgl_contexts: DomRefCell::new(HashSet::new()), + csp_list: DomRefCell::new(None), } } + pub fn set_csp_list(&self, csp_list: Option<CspList>) { + *self.csp_list.borrow_mut() = csp_list; + } + + pub fn get_csp_list(&self) -> Option<Ref<CspList>> { + ref_filter_map(self.csp_list.borrow(), Option::as_ref) + } + + /// https://www.w3.org/TR/CSP/#should-block-inline + pub fn should_elements_inline_type_behavior_be_blocked( + &self, + el: &Element, + type_: csp::InlineCheckType, + source: &str, + ) -> csp::CheckResult { + let element = csp::Element { + nonce: el + .get_attribute(&ns!(), &local_name!("nonce")) + .map(|attr| Cow::Owned(attr.value().to_string())), + }; + // TODO: Instead of ignoring violations, report them. + self.get_csp_list() + .map(|c| { + c.should_elements_inline_type_behavior_be_blocked(&element, type_, source) + .0 + }) + .unwrap_or(csp::CheckResult::Allowed) + } + /// Prevent any JS or layout from running until the corresponding call to /// `remove_script_and_layout_blocker`. Used to isolate periods in which /// the DOM is in an unstable state and should not be exposed to arbitrary diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 2ae306e25be..c7635e98463 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -38,6 +38,7 @@ use crate::task_source::websocket::WebsocketTaskSource; use crate::task_source::TaskSourceName; use crate::timers::{IsInterval, OneshotTimerCallback, OneshotTimerHandle}; use crate::timers::{OneshotTimers, TimerCallback}; +use content_security_policy::CspList; use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId}; use dom_struct::dom_struct; use ipc_channel::ipc::IpcSender; @@ -812,6 +813,15 @@ impl GlobalScope { pub fn get_user_agent(&self) -> Cow<'static, str> { self.user_agent.clone() } + + /// https://www.w3.org/TR/CSP/#get-csp-of-object + pub fn get_csp_list(&self) -> Option<CspList> { + if let Some(window) = self.downcast::<Window>() { + return window.Document().get_csp_list().map(|c| c.clone()); + } + // TODO: Worker and Worklet global scopes. + None + } } fn timestamp_in_ms(time: Timespec) -> u64 { diff --git a/components/script/dom/htmlscriptelement.rs b/components/script/dom/htmlscriptelement.rs index 1bd0101c03d..dfac55a6f66 100644 --- a/components/script/dom/htmlscriptelement.rs +++ b/components/script/dom/htmlscriptelement.rs @@ -27,6 +27,7 @@ use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::virtualmethods::VirtualMethods; use crate::fetch::create_a_potential_CORS_request; use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingListener}; +use content_security_policy as csp; use dom_struct::dom_struct; use encoding_rs::Encoding; use html5ever::{LocalName, Prefix}; @@ -428,7 +429,16 @@ impl HTMLScriptElement { // TODO: Step 12: nomodule content attribute - // TODO(#4577): Step 13: CSP. + // Step 13. + if !element.has_attribute(&local_name!("src")) && + doc.should_elements_inline_type_behavior_be_blocked( + &element, + csp::InlineCheckType::Script, + &text, + ) == csp::CheckResult::Blocked + { + return; + } // Step 14. let for_attribute = element.get_attribute(&ns!(), &local_name!("for")); diff --git a/components/script/dom/request.rs b/components/script/dom/request.rs index 5393288c633..d22d4680355 100644 --- a/components/script/dom/request.rs +++ b/components/script/dom/request.rs @@ -755,7 +755,9 @@ impl Into<RequestDestination> for NetTraitsRequestDestination { NetTraitsRequestDestination::Object => RequestDestination::Object, NetTraitsRequestDestination::Report => RequestDestination::Report, NetTraitsRequestDestination::Script => RequestDestination::Script, - NetTraitsRequestDestination::ServiceWorker => { + NetTraitsRequestDestination::ServiceWorker | + NetTraitsRequestDestination::AudioWorklet | + NetTraitsRequestDestination::PaintWorklet => { panic!("ServiceWorker request destination should not be exposed to DOM") }, NetTraitsRequestDestination::SharedWorker => RequestDestination::Sharedworker, diff --git a/components/script/dom/servoparser/mod.rs b/components/script/dom/servoparser/mod.rs index a05dea2a66d..91b5c9bfce6 100644 --- a/components/script/dom/servoparser/mod.rs +++ b/components/script/dom/servoparser/mod.rs @@ -35,6 +35,7 @@ use crate::dom::text::Text; use crate::dom::virtualmethods::vtable_for; use crate::network_listener::PreInvoke; use crate::script_thread::ScriptThread; +use content_security_policy::{self as csp, CspList}; use dom_struct::dom_struct; use embedder_traits::resources::{self, Resource}; use encoding_rs::Encoding; @@ -736,6 +737,31 @@ impl FetchResponseListener for ParserContext { .and_then(|meta| meta.content_type) .map(Serde::into_inner) .map(Into::into); + + // https://www.w3.org/TR/CSP/#initialize-document-csp + // TODO: Implement step 1 (local scheme special case) + let csp_list = metadata.as_ref().and_then(|m| { + let h = m.headers.as_ref()?; + let mut csp = h.get_all("content-security-policy").iter(); + // This silently ignores the CSP if it contains invalid Unicode. + // We should probably report an error somewhere. + let c = csp.next().and_then(|c| c.to_str().ok())?; + let mut csp_list = CspList::parse( + c, + csp::PolicySource::Header, + csp::PolicyDisposition::Enforce, + ); + for c in csp { + let c = c.to_str().ok()?; + csp_list.append(CspList::parse( + c, + csp::PolicySource::Header, + csp::PolicyDisposition::Enforce, + )); + } + Some(csp_list) + }); + let parser = match ScriptThread::page_headers_available(&self.id, metadata) { Some(parser) => parser, None => return, @@ -744,6 +770,8 @@ impl FetchResponseListener for ParserContext { return; } + parser.document.set_csp_list(csp_list); + self.parser = Some(Trusted::new(&*parser)); match content_type { diff --git a/components/script/fetch.rs b/components/script/fetch.rs index 045e9b93eda..68285653a2f 100644 --- a/components/script/fetch.rs +++ b/components/script/fetch.rs @@ -127,6 +127,7 @@ fn request_init_from_request(request: NetTraitsRequest) -> RequestBuilder { url_list: vec![], parser_metadata: request.parser_metadata, initiator: request.initiator, + csp_list: None, } } @@ -155,6 +156,7 @@ pub fn Fetch( let timing_type = request.timing_type(); let mut request_init = request_init_from_request(request); + request_init.csp_list = global.get_csp_list().clone(); // Step 3 if global.downcast::<ServiceWorkerGlobalScope>().is_some() { diff --git a/tests/wpt/metadata/fetch/api/policies/csp-blocked.html.ini b/tests/wpt/metadata/fetch/api/policies/csp-blocked.html.ini deleted file mode 100644 index 72555302f16..00000000000 --- a/tests/wpt/metadata/fetch/api/policies/csp-blocked.html.ini +++ /dev/null @@ -1,5 +0,0 @@ -[csp-blocked.html] - type: testharness - [Fetch is blocked by CSP, got a TypeError] - expected: FAIL - |