diff options
-rw-r--r-- | Cargo.lock | 18 | ||||
-rw-r--r-- | components/net/fetch/methods.rs | 78 | ||||
-rw-r--r-- | components/net/http_loader.rs | 2 | ||||
-rw-r--r-- | components/net/lib.rs | 2 | ||||
-rw-r--r-- | components/net/subresource_integrity.rs | 177 | ||||
-rw-r--r-- | components/net_traits/request.rs | 3 | ||||
-rw-r--r-- | components/script/dom/htmllinkelement.rs | 16 | ||||
-rw-r--r-- | components/script/dom/htmlscriptelement.rs | 18 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLLinkElement.webidl | 1 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLScriptElement.webidl | 1 | ||||
-rw-r--r-- | components/script/stylesheet_loader.rs | 5 | ||||
-rw-r--r-- | tests/unit/net/fetch.rs | 49 | ||||
-rw-r--r-- | tests/unit/net/lib.rs | 1 | ||||
-rw-r--r-- | tests/unit/net/subresource_integrity.rs | 92 | ||||
-rw-r--r-- | tests/wpt/include.ini | 2 | ||||
-rw-r--r-- | tests/wpt/metadata/html/dom/interfaces.html.ini | 6 | ||||
-rw-r--r-- | tests/wpt/metadata/html/dom/reflection-metadata.html.ini | 96 | ||||
-rw-r--r-- | tests/wpt/metadata/html/dom/reflection-misc.html.ini | 96 | ||||
-rw-r--r-- | tests/wpt/metadata/subresource-integrity/subresource-integrity.sub.html.ini | 36 |
19 files changed, 439 insertions, 260 deletions
diff --git a/Cargo.lock b/Cargo.lock index c8186923756..5edcc72af80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "heapsize 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mac 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1121,7 +1121,7 @@ dependencies = [ [[package]] name = "html5ever-atoms" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "heapsize 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1306,7 +1306,7 @@ dependencies = [ "gfx_traits 0.0.1", "heapsize 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "ipc-channel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2264,7 +2264,7 @@ dependencies = [ "heapsize 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "html5ever 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)", "hyper_serde 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2324,7 +2324,7 @@ dependencies = [ "gfx_traits 0.0.1", "heapsize 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "ipc-channel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2740,7 +2740,7 @@ dependencies = [ "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize_derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "libbindgen 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2779,7 +2779,7 @@ dependencies = [ "app_units 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "cssparser 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "euclid 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3312,7 +3312,7 @@ name = "xml5ever" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mac 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3414,7 +3414,7 @@ dependencies = [ "checksum heartbeats-simple-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53c4b67617665d7f4172f381f9843c1bec6a4fccc9a9226529e5b1be40dc1301" "checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58" "checksum html5ever 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a358fef34c3334e92cd34d83ce870a386334e605e7abe987a69a7078a4142c69" -"checksum html5ever-atoms 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fd3fc831590ee7fcf693c673e4e3cbe14fbda44dc0f26d9bdc79cfc9f551dc05" +"checksum html5ever-atoms 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4109e35fec157307b918eb9d5b7018e2fa771aea0c04831e22003ac4722fbd1b" "checksum httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e7a63e511f9edffbab707141fbb8707d1a3098615fb2adbd5769cdfcc9b17d" "checksum hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)" = "bcb3fc65554155980167fb821d05c7c66177f92464976c0b676a19d9e03387a7" "checksum hyper_serde 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "572d2168173019de312a050a24f2ad33ac2ac7895a2139fbf21ee6b6f470a24e" diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 0a284ee4016..44a603025b0 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -24,6 +24,7 @@ use std::io::Read; use std::mem; use std::rc::Rc; use std::sync::mpsc::{Sender, Receiver}; +use subresource_integrity::is_response_integrity_valid; pub type Target<'a> = &'a mut (FetchTaskTarget + Send); @@ -268,6 +269,7 @@ pub fn main_fetch(request: Rc<Request>, response }; + let mut response_loaded = false; { // Step 14 let network_error_res; @@ -297,50 +299,33 @@ pub fn main_fetch(request: Rc<Request>, let mut body = internal_response.body.lock().unwrap(); *body = ResponseBody::Empty; } - - // Step 18 - // TODO be able to compare response integrity against request integrity metadata - // if !response.is_network_error() { - - // // Substep 1 - // response.wait_until_done(); - - // // Substep 2 - // if response.termination_reason.is_none() { - // response = Response::network_error(); - // internal_response = Response::network_error(); - // } - // } } + // Step 18 + let response = if !response.is_network_error() && *request.integrity_metadata.borrow() != "" { + // Substep 1 + wait_for_response(&response, target, done_chan); + response_loaded = true; + + // Substep 2 + let ref integrity_metadata = *request.integrity_metadata.borrow(); + if response.termination_reason.is_none() && + !is_response_integrity_valid(integrity_metadata, &response) { + Response::network_error(NetworkError::Internal("Subresource integrity validation failed".into())) + } else { + response + } + } else { + response + }; // Step 19 if request.synchronous { // process_response is not supposed to be used // by sync fetch, but we overload it here for simplicity target.process_response(&response); - - if let Some(ref ch) = *done_chan { - loop { - match ch.1.recv() - .expect("fetch worker should always send Done before terminating") { - Data::Payload(vec) => { - target.process_response_chunk(vec); - } - Data::Done => break, - } - } - } else { - let body = response.body.lock().unwrap(); - if let ResponseBody::Done(ref vec) = *body { - // in case there was no channel to wait for, the body was - // obtained synchronously via basic_fetch for data/file/about/etc - // We should still send the body across as a chunk - target.process_response_chunk(vec.clone()); - } else { - assert!(*body == ResponseBody::Empty) - } + if !response_loaded { + wait_for_response(&response, target, done_chan); } - // overloaded similarly to process_response target.process_response_eof(&response); return response; @@ -360,13 +345,25 @@ pub fn main_fetch(request: Rc<Request>, target.process_response(&response); // Step 22 + if !response_loaded { + wait_for_response(&response, target, done_chan); + } + + // Step 24 + target.process_response_eof(&response); + + // TODO remove this line when only asynchronous fetches are used + return response; +} + +fn wait_for_response(response: &Response, target: Target, done_chan: &mut DoneChannel) { if let Some(ref ch) = *done_chan { loop { match ch.1.recv() .expect("fetch worker should always send Done before terminating") { Data::Payload(vec) => { target.process_response_chunk(vec); - } + }, Data::Done => break, } } @@ -381,12 +378,6 @@ pub fn main_fetch(request: Rc<Request>, assert!(*body == ResponseBody::Empty) } } - - // Step 24 - target.process_response_eof(&response); - - // TODO remove this line when only asynchronous fetches are used - return response; } /// [Basic fetch](https://fetch.spec.whatwg.org#basic-fetch) @@ -518,3 +509,4 @@ fn is_null_body_status(status: &Option<StatusCode>) -> bool { _ => false } } + diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index de066b8248e..63fa745707b 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -1378,7 +1378,7 @@ fn cors_check(request: Rc<Request>, response: &Response) -> Result<(), ()> { } // Step 6 - let credentials = request.headers.borrow().get::<AccessControlAllowCredentials>().cloned(); + let credentials = response.headers.get::<AccessControlAllowCredentials>().cloned(); // Step 7 if credentials.is_some() { diff --git a/components/net/lib.rs b/components/net/lib.rs index 098c0ba540c..57ff2af1a0e 100644 --- a/components/net/lib.rs +++ b/components/net/lib.rs @@ -60,8 +60,8 @@ pub mod image_cache_thread; pub mod mime_classifier; pub mod resource_thread; mod storage_thread; +pub mod subresource_integrity; mod websocket_loader; - /// An implementation of the [Fetch specification](https://fetch.spec.whatwg.org/) pub mod fetch { pub mod cors_cache; diff --git a/components/net/subresource_integrity.rs b/components/net/subresource_integrity.rs new file mode 100644 index 00000000000..7268edbf80e --- /dev/null +++ b/components/net/subresource_integrity.rs @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use net_traits::response::{Response, ResponseBody, ResponseType}; +use openssl::crypto::hash::{hash, Type as MessageDigest}; +use rustc_serialize::base64::{STANDARD, ToBase64}; +use std::iter::Filter; +use std::str::Split; +use std::sync::MutexGuard; +const SUPPORTED_ALGORITHM: &'static [&'static str] = &[ + "sha256", + "sha384", + "sha512", +]; +pub type StaticCharVec = &'static [char]; +/// A "space character" according to: +/// +/// https://html.spec.whatwg.org/multipage/#space-character +pub static HTML_SPACE_CHARACTERS: StaticCharVec = &[ + '\u{0020}', + '\u{0009}', + '\u{000a}', + '\u{000c}', + '\u{000d}', +]; +#[derive(Clone)] +pub struct SriEntry { + pub alg: String, + pub val: String, + // TODO : Current version of spec does not define any option. + // Can be refactored into appropriate datastructure when future + // spec has more details. + pub opt: Option<String>, +} + +impl SriEntry { + pub fn new(alg: &str, val: &str, opt: Option<String>) -> SriEntry { + SriEntry { + alg: alg.to_owned(), + val: val.to_owned(), + opt: opt, + } + } +} + +/// https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata +pub fn parsed_metadata(integrity_metadata: &str) -> Vec<SriEntry> { + // Step 1 + let mut result = vec![]; + + // Step 3 + let tokens = split_html_space_chars(integrity_metadata); + for token in tokens { + let parsed_data: Vec<&str> = token.split("-").collect(); + + if parsed_data.len() > 1 { + let alg = parsed_data[0]; + + if !SUPPORTED_ALGORITHM.contains(&alg) { + continue; + } + + let data: Vec<&str> = parsed_data[1].split("?").collect(); + let digest = data[0]; + + let opt = if data.len() > 1 { + Some(data[1].to_owned()) + } else { + None + }; + + result.push(SriEntry::new(alg, digest, opt)); + } + } + + return result; +} + +/// https://w3c.github.io/webappsec-subresource-integrity/#getprioritizedhashfunction +pub fn get_prioritized_hash_function(hash_func_left: &str, hash_func_right: &str) -> Option<String> { + let left_priority = SUPPORTED_ALGORITHM.iter().position(|s| s.to_owned() == hash_func_left).unwrap(); + let right_priority = SUPPORTED_ALGORITHM.iter().position(|s| s.to_owned() == hash_func_right).unwrap(); + + if left_priority == right_priority { + return None; + } + if left_priority > right_priority { + Some(hash_func_left.to_owned()) + } else { + Some(hash_func_right.to_owned()) + } + +} + +/// https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata +pub fn get_strongest_metadata(integrity_metadata_list: Vec<SriEntry>) -> Vec<SriEntry> { + let mut result: Vec<SriEntry> = vec![integrity_metadata_list[0].clone()]; + let mut current_algorithm = result[0].alg.clone(); + + for integrity_metadata in &integrity_metadata_list[1..] { + let prioritized_hash = get_prioritized_hash_function(&integrity_metadata.alg, + &*current_algorithm); + if prioritized_hash.is_none() { + result.push(integrity_metadata.clone()); + } else if let Some(algorithm) = prioritized_hash { + if algorithm != current_algorithm { + result = vec![integrity_metadata.clone()]; + current_algorithm = algorithm; + } + } + } + + result +} + +/// https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response +fn apply_algorithm_to_response(body: MutexGuard<ResponseBody>, + message_digest: MessageDigest) + -> String { + if let ResponseBody::Done(ref vec) = *body { + let response_digest = hash(message_digest, vec); + response_digest.to_base64(STANDARD) + } else { + unreachable!("Tried to calculate digest of incomplete response body") + } +} + +/// https://w3c.github.io/webappsec-subresource-integrity/#is-response-eligible +fn is_eligible_for_integrity_validation(response: &Response) -> bool { + match response.response_type { + ResponseType::Basic | ResponseType::Default | ResponseType::Cors => true, + _ => false, + } +} + +/// https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist +pub fn is_response_integrity_valid(integrity_metadata: &str, response: &Response) -> bool { + let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata); + + // Step 2 & 4 + if parsed_metadata_list.is_empty() { + return true; + } + + // Step 3 + if !is_eligible_for_integrity_validation(response) { + return false; + } + + // Step 5 + let metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list); + for item in metadata { + let body = response.body.lock().unwrap(); + let algorithm = item.alg; + let digest = item.val; + + let message_digest = match &*algorithm { + "sha256" => MessageDigest::SHA256, + "sha384" => MessageDigest::SHA384, + "sha512" => MessageDigest::SHA512, + _ => continue, + }; + + if apply_algorithm_to_response(body, message_digest) == digest { + return true; + } + } + + false +} + +pub fn split_html_space_chars<'a>(s: &'a str) -> + Filter<Split<'a, StaticCharVec>, fn(&&str) -> bool> { + fn not_empty(&split: &&str) -> bool { !split.is_empty() } + s.split(HTML_SPACE_CHARACTERS).filter(not_empty as fn(&&str) -> bool) +} diff --git a/components/net_traits/request.rs b/components/net_traits/request.rs index d082f20b422..13a0ece3c0b 100644 --- a/components/net_traits/request.rs +++ b/components/net_traits/request.rs @@ -158,6 +158,7 @@ pub struct RequestInit { pub referrer_policy: Option<ReferrerPolicy>, pub pipeline_id: Option<PipelineId>, pub redirect_mode: RedirectMode, + pub integrity_metadata: String, } impl Default for RequestInit { @@ -181,6 +182,7 @@ impl Default for RequestInit { referrer_policy: None, pipeline_id: None, redirect_mode: RedirectMode::Follow, + integrity_metadata: "".to_owned(), } } } @@ -291,6 +293,7 @@ impl Request { req.referrer_policy.set(init.referrer_policy); req.pipeline_id.set(init.pipeline_id); req.redirect_mode.set(init.redirect_mode); + *req.integrity_metadata.borrow_mut() = init.integrity_metadata; req } diff --git a/components/script/dom/htmllinkelement.rs b/components/script/dom/htmllinkelement.rs index ee4ee4718cf..24b4e779099 100644 --- a/components/script/dom/htmllinkelement.rs +++ b/components/script/dom/htmllinkelement.rs @@ -243,16 +243,24 @@ impl HTMLLinkElement { Some(ref value) => &***value, None => "", }; + let mut css_parser = CssParser::new(&mq_str); let media = parse_media_query_list(&mut css_parser); + let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity")); + let integrity_val = im_attribute.r().map(|a| a.value()); + let integrity_metadata = match integrity_val { + Some(ref value) => &***value, + None => "", + }; + // TODO: #8085 - Don't load external stylesheets if the node's mq // doesn't match. let loader = StylesheetLoader::for_element(self.upcast()); loader.load(StylesheetContextSource::LinkElement { url: url, media: Some(media), - }); + }, integrity_metadata.to_owned()); } fn handle_favicon_url(&self, rel: &str, href: &str, sizes: &Option<String>) { @@ -328,6 +336,12 @@ impl HTMLLinkElementMethods for HTMLLinkElement { // https://html.spec.whatwg.org/multipage/#dom-link-media make_setter!(SetMedia, "media"); + // https://html.spec.whatwg.org/multipage/#dom-link-integrity + make_getter!(Integrity, "integrity"); + + // https://html.spec.whatwg.org/multipage/#dom-link-integrity + make_setter!(SetIntegrity, "integrity"); + // https://html.spec.whatwg.org/multipage/#dom-link-hreflang make_getter!(Hreflang, "hreflang"); diff --git a/components/script/dom/htmlscriptelement.rs b/components/script/dom/htmlscriptelement.rs index 9558d9c4b41..9c2bcfd583c 100644 --- a/components/script/dom/htmlscriptelement.rs +++ b/components/script/dom/htmlscriptelement.rs @@ -40,7 +40,6 @@ use std::ascii::AsciiExt; use std::cell::Cell; use std::sync::{Arc, Mutex}; use style::str::{HTML_SPACE_CHARACTERS, StaticStringVec}; - #[dom_struct] pub struct HTMLScriptElement { htmlelement: HTMLElement, @@ -221,6 +220,7 @@ impl PreInvoke for ScriptContext {} fn fetch_a_classic_script(script: &HTMLScriptElement, url: ServoUrl, cors_setting: Option<CorsSettings>, + integrity_metadata: String, character_encoding: EncodingRef) { let doc = document_from_node(script); @@ -245,6 +245,7 @@ fn fetch_a_classic_script(script: &HTMLScriptElement, pipeline_id: Some(script.global().pipeline_id()), referrer_url: Some(doc.url()), referrer_policy: doc.get_referrer_policy(), + integrity_metadata: integrity_metadata, .. RequestInit::default() }; @@ -365,7 +366,13 @@ impl HTMLScriptElement { // TODO: Step 15: Nonce. - // TODO: Step 16: Parser state. + // Step 16: Integrity Metadata + let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity")); + let integrity_val = im_attribute.r().map(|a| a.value()); + let integrity_metadata = match integrity_val { + Some(ref value) => &***value, + None => "", + }; // TODO: Step 17: environment settings object. @@ -393,7 +400,7 @@ impl HTMLScriptElement { }; // Step 18.6. - fetch_a_classic_script(self, url, cors_setting, encoding); + fetch_a_classic_script(self, url, cors_setting, integrity_metadata.to_owned(), encoding); true }, @@ -675,6 +682,11 @@ impl HTMLScriptElementMethods for HTMLScriptElement { // https://html.spec.whatwg.org/multipage/#dom-script-defer make_bool_setter!(SetDefer, "defer"); + // https://html.spec.whatwg.org/multipage/#dom-script-integrity + make_getter!(Integrity, "integrity"); + // https://html.spec.whatwg.org/multipage/#dom-script-integrity + make_setter!(SetIntegrity, "integrity"); + // https://html.spec.whatwg.org/multipage/#dom-script-event make_getter!(Event, "event"); // https://html.spec.whatwg.org/multipage/#dom-script-event diff --git a/components/script/dom/webidls/HTMLLinkElement.webidl b/components/script/dom/webidls/HTMLLinkElement.webidl index e7bec059efe..46b611d3548 100644 --- a/components/script/dom/webidls/HTMLLinkElement.webidl +++ b/components/script/dom/webidls/HTMLLinkElement.webidl @@ -11,6 +11,7 @@ interface HTMLLinkElement : HTMLElement { attribute DOMString media; attribute DOMString hreflang; attribute DOMString type; + attribute DOMString integrity; // [SameObject, PutForwards=value] readonly attribute DOMTokenList sizes; // also has obsolete members diff --git a/components/script/dom/webidls/HTMLScriptElement.webidl b/components/script/dom/webidls/HTMLScriptElement.webidl index 4a0827800ed..0dcc9ddf69a 100644 --- a/components/script/dom/webidls/HTMLScriptElement.webidl +++ b/components/script/dom/webidls/HTMLScriptElement.webidl @@ -12,6 +12,7 @@ interface HTMLScriptElement : HTMLElement { attribute DOMString? crossOrigin; [Pure] attribute DOMString text; + attribute DOMString integrity; // also has obsolete members }; diff --git a/components/script/stylesheet_loader.rs b/components/script/stylesheet_loader.rs index 28c5e86b6b5..db70c9e8ebb 100644 --- a/components/script/stylesheet_loader.rs +++ b/components/script/stylesheet_loader.rs @@ -193,7 +193,7 @@ impl<'a> StylesheetLoader<'a> { } impl<'a> StylesheetLoader<'a> { - pub fn load(&self, source: StylesheetContextSource) { + pub fn load(&self, source: StylesheetContextSource, integrity_metadata: String) { let url = source.url(); let document = document_from_node(self.elem); let context = Arc::new(Mutex::new(StylesheetContext { @@ -234,6 +234,7 @@ impl<'a> StylesheetLoader<'a> { pipeline_id: Some(self.elem.global().pipeline_id()), referrer_url: Some(document.url()), referrer_policy: referrer_policy, + integrity_metadata: integrity_metadata, .. RequestInit::default() }; @@ -243,6 +244,6 @@ impl<'a> StylesheetLoader<'a> { impl<'a> StyleStylesheetLoader for StylesheetLoader<'a> { fn request_stylesheet(&self, import: &Arc<RwLock<ImportRule>>) { - self.load(StylesheetContextSource::Import(import.clone())) + self.load(StylesheetContextSource::Import(import.clone()), "".to_owned()) } } diff --git a/tests/unit/net/fetch.rs b/tests/unit/net/fetch.rs index 07bc5aeea4f..4329e44884d 100644 --- a/tests/unit/net/fetch.rs +++ b/tests/unit/net/fetch.rs @@ -233,7 +233,6 @@ fn test_cors_preflight_fetch() { let _ = server.close(); assert!(!fetch_response.is_network_error()); - match *fetch_response.body.lock().unwrap() { ResponseBody::Done(ref body) => assert_eq!(&**body, ACK), _ => panic!() @@ -556,6 +555,53 @@ fn test_fetch_with_hsts() { "https"); } +#[test] +fn test_fetch_with_sri_network_error() { + static MESSAGE: &'static [u8] = b"alert('Hello, Network Error');"; + let handler = move |_: HyperRequest, response: HyperResponse| { + response.send(MESSAGE).unwrap(); + }; + let (mut server, url) = make_server(handler); + + let origin = Origin::Origin(url.origin()); + let mut request = Request::new(url, Some(origin), false, None); + *request.referrer.borrow_mut() = Referrer::NoReferrer; + // To calulate hash use : + // echo -n "alert('Hello, Network Error');" | openssl dgst -sha384 -binary | openssl base64 -A + *request.integrity_metadata.borrow_mut() = + "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO".to_owned(); + // Set the flag. + request.local_urls_only = false; + + let response = fetch(request, None); + + let _ = server.close(); + assert!(response.is_network_error()); +} + +#[test] +fn test_fetch_with_sri_sucess() { + static MESSAGE: &'static [u8] = b"alert('Hello, world.');"; + let handler = move |_: HyperRequest, response: HyperResponse| { + response.send(MESSAGE).unwrap(); + }; + let (mut server, url) = make_server(handler); + + let origin = Origin::Origin(url.origin()); + let mut request = Request::new(url, Some(origin), false, None); + *request.referrer.borrow_mut() = Referrer::NoReferrer; + // To calulate hash use : + // echo -n "alert('Hello, Network Error');" | openssl dgst -sha384 -binary | openssl base64 -A + *request.integrity_metadata.borrow_mut() = + "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO".to_owned(); + // Set the flag. + request.local_urls_only = false; + + let response = fetch(request, None); + + let _ = server.close(); + assert_eq!(response_is_done(&response), true); +} fn setup_server_and_fetch(message: &'static [u8], redirect_cap: u32) -> Response { let handler = move |request: HyperRequest, mut response: HyperResponse| { let redirects = match request.uri { @@ -742,7 +788,6 @@ fn test_fetch_async_returns_complete_response() { let fetch_response = fetch(request, None); let _ = server.close(); - assert_eq!(response_is_done(&fetch_response), true); } diff --git a/tests/unit/net/lib.rs b/tests/unit/net/lib.rs index 990247e9ab2..47f732dc7b8 100644 --- a/tests/unit/net/lib.rs +++ b/tests/unit/net/lib.rs @@ -33,6 +33,7 @@ extern crate url; #[cfg(test)] mod hsts; #[cfg(test)] mod http_loader; #[cfg(test)] mod filemanager_thread; +#[cfg(test)] mod subresource_integrity; use devtools_traits::DevtoolsControlMsg; use hyper::server::{Handler, Listening, Server}; diff --git a/tests/unit/net/subresource_integrity.rs b/tests/unit/net/subresource_integrity.rs new file mode 100644 index 00000000000..d67da9c1775 --- /dev/null +++ b/tests/unit/net/subresource_integrity.rs @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use net::subresource_integrity::{SriEntry, get_prioritized_hash_function, get_strongest_metadata}; +use net::subresource_integrity::{is_response_integrity_valid, parsed_metadata}; +use net_traits::response::{Response, ResponseBody}; +use servo_url::ServoUrl; + +#[test] +fn test_get_prioritized_hash_function() { + let mut algorithm = get_prioritized_hash_function("sha256", "sha256"); + assert_eq!(algorithm, None); + + algorithm = get_prioritized_hash_function("sha256", "sha384"); + assert_eq!(algorithm.unwrap(), "sha384"); + + algorithm = get_prioritized_hash_function("sha384", "sha512"); + assert_eq!(algorithm.unwrap(), "sha512"); +} + +#[test] +fn test_parsed_metadata_without_options() { + let integrity_metadata = "sha384-Hash1"; + let ref parsed_metadata: SriEntry = parsed_metadata(integrity_metadata)[0]; + + assert_eq!(parsed_metadata.alg, "sha384"); + assert_eq!(parsed_metadata.val, "Hash1"); + assert!(parsed_metadata.opt.is_none()); +} + +#[test] +fn test_parsed_metadata_with_options() { + let integrity_metadata = "sha384-Hash1?opt=23"; + let ref parsed_metadata: SriEntry = parsed_metadata(integrity_metadata)[0]; + + assert_eq!(parsed_metadata.alg, "sha384"); + assert_eq!(parsed_metadata.val, "Hash1"); + assert!(parsed_metadata.opt.is_some()); +} + +#[test] +fn test_parsed_metadata_with_malformed_integrity() { + let integrity_metadata = "Not a valid integrity"; + let ref parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata); + + assert!(parsed_metadata_list.is_empty()); +} + +#[test] +fn test_get_strongest_metadata_two_same_algorithm() { + let integrity_metadata = "sha512-Hash1 sha512-Hash2?opt=23"; + let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata); + + let strong_metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list); + assert_eq!(strong_metadata.len(), 2); + assert_eq!(strong_metadata[0].alg, strong_metadata[1].alg); +} + +#[test] +fn test_get_strongest_metadata_different_algorithm() { + let integrity_metadata = "sha256-Hash0 sha384-Hash1 sha512-Hash2?opt=23"; + let parsed_metadata_list: Vec<SriEntry> = parsed_metadata(integrity_metadata); + + let strong_metadata: Vec<SriEntry> = get_strongest_metadata(parsed_metadata_list); + assert_eq!(strong_metadata.len(), 1); + assert_eq!(strong_metadata[0].alg, "sha512"); +} + +#[test] +fn test_response_integrity_valid() { + let url: ServoUrl = ServoUrl::parse("http://servo.org").unwrap(); + let response: Response = Response::new(url); + + let integrity_metadata = "sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO"; + let response_body = "alert('Hello, world.');".to_owned().into_bytes(); + + *response.body.lock().unwrap() = ResponseBody::Done(response_body); + assert!(is_response_integrity_valid(integrity_metadata, &response)); +} + +#[test] +fn test_response_integrity_invalid() { + let url: ServoUrl = ServoUrl::parse("http://servo.org").unwrap(); + let response: Response = Response::new(url); + + let integrity_metadata = "sha256-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO"; + let response_body = "alert('Hello, world.');".to_owned().into_bytes(); + + *response.body.lock().unwrap() = ResponseBody::Done(response_body); + assert!(!is_response_integrity_valid(integrity_metadata, &response)); +} diff --git a/tests/wpt/include.ini b/tests/wpt/include.ini index 63631c662cb..f84dc09d350 100644 --- a/tests/wpt/include.ini +++ b/tests/wpt/include.ini @@ -43,6 +43,8 @@ skip: true skip: false [referrer-policy] skip: false +[subresource-integrity] + skip: false [touch-events] skip: false [typedarrays] diff --git a/tests/wpt/metadata/html/dom/interfaces.html.ini b/tests/wpt/metadata/html/dom/interfaces.html.ini index 54293b1a360..4de1ed5423f 100644 --- a/tests/wpt/metadata/html/dom/interfaces.html.ini +++ b/tests/wpt/metadata/html/dom/interfaces.html.ini @@ -9870,18 +9870,12 @@ [HTMLLinkElement interface: attribute nonce] expected: FAIL - [HTMLLinkElement interface: attribute integrity] - expected: FAIL - [HTMLLinkElement interface: attribute referrerPolicy] expected: FAIL [HTMLLinkElement interface: document.createElement("link") must inherit property "nonce" with the proper type (5)] expected: FAIL - [HTMLLinkElement interface: document.createElement("link") must inherit property "integrity" with the proper type (6)] - expected: FAIL - [HTMLLinkElement interface: document.createElement("link") must inherit property "sizes" with the proper type (9)] expected: FAIL diff --git a/tests/wpt/metadata/html/dom/reflection-metadata.html.ini b/tests/wpt/metadata/html/dom/reflection-metadata.html.ini index b4c8344e29c..5d2c8295e81 100644 --- a/tests/wpt/metadata/html/dom/reflection-metadata.html.ini +++ b/tests/wpt/metadata/html/dom/reflection-metadata.html.ini @@ -10116,102 +10116,6 @@ [link.nonce: IDL set to object "test-valueOf"] expected: FAIL - [link.integrity: typeof IDL attribute] - expected: FAIL - - [link.integrity: IDL get with DOM attribute unset] - expected: FAIL - - [link.integrity: setAttribute() to ""] - expected: FAIL - - [link.integrity: setAttribute() to " \\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07 \\b\\t\\n\\v\\f\\r\\x0e\\x0f \\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17 \\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f foo "] - expected: FAIL - - [link.integrity: setAttribute() to undefined] - expected: FAIL - - [link.integrity: setAttribute() to 7] - expected: FAIL - - [link.integrity: setAttribute() to 1.5] - expected: FAIL - - [link.integrity: setAttribute() to true] - expected: FAIL - - [link.integrity: setAttribute() to false] - expected: FAIL - - [link.integrity: setAttribute() to object "[object Object\]"] - expected: FAIL - - [link.integrity: setAttribute() to NaN] - expected: FAIL - - [link.integrity: setAttribute() to Infinity] - expected: FAIL - - [link.integrity: setAttribute() to -Infinity] - expected: FAIL - - [link.integrity: setAttribute() to "\\0"] - expected: FAIL - - [link.integrity: setAttribute() to null] - expected: FAIL - - [link.integrity: setAttribute() to object "test-toString"] - expected: FAIL - - [link.integrity: setAttribute() to object "test-valueOf"] - expected: FAIL - - [link.integrity: IDL set to ""] - expected: FAIL - - [link.integrity: IDL set to " \\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07 \\b\\t\\n\\v\\f\\r\\x0e\\x0f \\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17 \\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f foo "] - expected: FAIL - - [link.integrity: IDL set to undefined] - expected: FAIL - - [link.integrity: IDL set to 7] - expected: FAIL - - [link.integrity: IDL set to 1.5] - expected: FAIL - - [link.integrity: IDL set to true] - expected: FAIL - - [link.integrity: IDL set to false] - expected: FAIL - - [link.integrity: IDL set to object "[object Object\]"] - expected: FAIL - - [link.integrity: IDL set to NaN] - expected: FAIL - - [link.integrity: IDL set to Infinity] - expected: FAIL - - [link.integrity: IDL set to -Infinity] - expected: FAIL - - [link.integrity: IDL set to "\\0"] - expected: FAIL - - [link.integrity: IDL set to null] - expected: FAIL - - [link.integrity: IDL set to object "test-toString"] - expected: FAIL - - [link.integrity: IDL set to object "test-valueOf"] - expected: FAIL - [link.referrerPolicy: typeof IDL attribute] expected: FAIL diff --git a/tests/wpt/metadata/html/dom/reflection-misc.html.ini b/tests/wpt/metadata/html/dom/reflection-misc.html.ini index d5201c817db..8cfb4aab53b 100644 --- a/tests/wpt/metadata/html/dom/reflection-misc.html.ini +++ b/tests/wpt/metadata/html/dom/reflection-misc.html.ini @@ -17610,99 +17610,3 @@ [script.nonce: IDL set to object "test-valueOf"] expected: FAIL - [script.integrity: typeof IDL attribute] - expected: FAIL - - [script.integrity: IDL get with DOM attribute unset] - expected: FAIL - - [script.integrity: setAttribute() to ""] - expected: FAIL - - [script.integrity: setAttribute() to " \\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07 \\b\\t\\n\\v\\f\\r\\x0e\\x0f \\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17 \\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f foo "] - expected: FAIL - - [script.integrity: setAttribute() to undefined] - expected: FAIL - - [script.integrity: setAttribute() to 7] - expected: FAIL - - [script.integrity: setAttribute() to 1.5] - expected: FAIL - - [script.integrity: setAttribute() to true] - expected: FAIL - - [script.integrity: setAttribute() to false] - expected: FAIL - - [script.integrity: setAttribute() to object "[object Object\]"] - expected: FAIL - - [script.integrity: setAttribute() to NaN] - expected: FAIL - - [script.integrity: setAttribute() to Infinity] - expected: FAIL - - [script.integrity: setAttribute() to -Infinity] - expected: FAIL - - [script.integrity: setAttribute() to "\\0"] - expected: FAIL - - [script.integrity: setAttribute() to null] - expected: FAIL - - [script.integrity: setAttribute() to object "test-toString"] - expected: FAIL - - [script.integrity: setAttribute() to object "test-valueOf"] - expected: FAIL - - [script.integrity: IDL set to ""] - expected: FAIL - - [script.integrity: IDL set to " \\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07 \\b\\t\\n\\v\\f\\r\\x0e\\x0f \\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17 \\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f foo "] - expected: FAIL - - [script.integrity: IDL set to undefined] - expected: FAIL - - [script.integrity: IDL set to 7] - expected: FAIL - - [script.integrity: IDL set to 1.5] - expected: FAIL - - [script.integrity: IDL set to true] - expected: FAIL - - [script.integrity: IDL set to false] - expected: FAIL - - [script.integrity: IDL set to object "[object Object\]"] - expected: FAIL - - [script.integrity: IDL set to NaN] - expected: FAIL - - [script.integrity: IDL set to Infinity] - expected: FAIL - - [script.integrity: IDL set to -Infinity] - expected: FAIL - - [script.integrity: IDL set to "\\0"] - expected: FAIL - - [script.integrity: IDL set to null] - expected: FAIL - - [script.integrity: IDL set to object "test-toString"] - expected: FAIL - - [script.integrity: IDL set to object "test-valueOf"] - expected: FAIL - diff --git a/tests/wpt/metadata/subresource-integrity/subresource-integrity.sub.html.ini b/tests/wpt/metadata/subresource-integrity/subresource-integrity.sub.html.ini new file mode 100644 index 00000000000..164756ce462 --- /dev/null +++ b/tests/wpt/metadata/subresource-integrity/subresource-integrity.sub.html.ini @@ -0,0 +1,36 @@ +[subresource-integrity.sub.html] + type: testharness + expected: TIMEOUT + [Style: <crossorigin='anonymous'> with correct hash, ACAO: *] + expected: FAIL + + [Style: Same-origin with correct sha256 and sha512 hash, rel='alternate stylesheet' enabled] + expected: NOTRUN + + [Style: Same-origin with incorrect sha256 and sha512 hash, rel='alternate stylesheet' enabled] + expected: NOTRUN + + [Style: Same-origin with incorrect hash.] + expected: FAIL + + [Style: Same-origin with sha256 match, sha512 mismatch] + expected: FAIL + + [Style: <crossorigin='use-credentials'> with correct hash, CORS-eligible] + expected: FAIL + + [Style: <crossorigin='anonymous'> with CORS-ineligible resource] + expected: FAIL + + [Style: Cross-origin, not CORS request, with correct hash] + expected: FAIL + + [Style: Cross-origin, not CORS request, with hash mismatch] + expected: FAIL + + [Style: <crossorigin='use-credentials'> with incorrect hash CORS-eligible] + expected: FAIL + + [Style: <crossorigin='anonymous'> with incorrect hash, ACAO: *] + expected: FAIL + |