mirror of
https://github.com/servo/servo.git
synced 2025-08-02 20:20:14 +01:00
Implemented nosniff for fetch algorithm
This commit is contained in:
parent
7cd4c69c40
commit
8aac575019
9 changed files with 172 additions and 79 deletions
|
@ -8,8 +8,10 @@ use devtools_traits::DevtoolsControlMsg;
|
||||||
use fetch::cors_cache::CorsCache;
|
use fetch::cors_cache::CorsCache;
|
||||||
use filemanager_thread::FileManager;
|
use filemanager_thread::FileManager;
|
||||||
use http_loader::{HttpState, determine_request_referrer, http_fetch, set_default_accept_language};
|
use http_loader::{HttpState, determine_request_referrer, http_fetch, set_default_accept_language};
|
||||||
|
use hyper::Error;
|
||||||
|
use hyper::error::Result as HyperResult;
|
||||||
use hyper::header::{Accept, AcceptLanguage, ContentLanguage, ContentType};
|
use hyper::header::{Accept, AcceptLanguage, ContentLanguage, ContentType};
|
||||||
use hyper::header::{HeaderView, QualityItem, Referer as RefererHeader, q, qitem};
|
use hyper::header::{Header, HeaderFormat, HeaderView, QualityItem, Referer as RefererHeader, q, qitem};
|
||||||
use hyper::method::Method;
|
use hyper::method::Method;
|
||||||
use hyper::mime::{Mime, SubLevel, TopLevel};
|
use hyper::mime::{Mime, SubLevel, TopLevel};
|
||||||
use hyper::status::StatusCode;
|
use hyper::status::StatusCode;
|
||||||
|
@ -19,10 +21,12 @@ use net_traits::request::{RedirectMode, Referrer, Request, RequestMode, Response
|
||||||
use net_traits::request::{Type, Origin, Window};
|
use net_traits::request::{Type, Origin, Window};
|
||||||
use net_traits::response::{Response, ResponseBody, ResponseType};
|
use net_traits::response::{Response, ResponseBody, ResponseType};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::str;
|
||||||
use std::sync::mpsc::{Sender, Receiver};
|
use std::sync::mpsc::{Sender, Receiver};
|
||||||
use subresource_integrity::is_response_integrity_valid;
|
use subresource_integrity::is_response_integrity_valid;
|
||||||
|
|
||||||
|
@ -269,13 +273,12 @@ pub fn main_fetch(request: Rc<Request>,
|
||||||
response
|
response
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut response_loaded = false;
|
let internal_error = { // Inner scope to reborrow response
|
||||||
{
|
|
||||||
// Step 14
|
// Step 14
|
||||||
let network_error_res;
|
let network_error_response;
|
||||||
let internal_response = if let Some(error) = response.get_network_error() {
|
let internal_response = if let Some(error) = response.get_network_error() {
|
||||||
network_error_res = Response::network_error(error.clone());
|
network_error_response = Response::network_error(error.clone());
|
||||||
&network_error_res
|
&network_error_response
|
||||||
} else {
|
} else {
|
||||||
response.actual_response()
|
response.actual_response()
|
||||||
};
|
};
|
||||||
|
@ -286,21 +289,41 @@ pub fn main_fetch(request: Rc<Request>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 16
|
// Step 16
|
||||||
// TODO this step (CSP/blocking)
|
// TODO Blocking for CSP, mixed content, MIME type
|
||||||
|
let blocked_error_response;
|
||||||
|
let internal_response = if !response.is_network_error() && should_block_nosniff(&request, &response) {
|
||||||
|
// Defer rebinding result
|
||||||
|
blocked_error_response = Response::network_error(NetworkError::Internal("Blocked by nosniff".into()));
|
||||||
|
&blocked_error_response
|
||||||
|
} else {
|
||||||
|
internal_response
|
||||||
|
};
|
||||||
|
|
||||||
// Step 17
|
// Step 17
|
||||||
if !response.is_network_error() && (is_null_body_status(&internal_response.status) ||
|
// We check `internal_response` since we did not mutate `response` in the previous step.
|
||||||
|
let not_network_error = !response.is_network_error() && !internal_response.is_network_error();
|
||||||
|
if not_network_error && (is_null_body_status(&internal_response.status) ||
|
||||||
match *request.method.borrow() {
|
match *request.method.borrow() {
|
||||||
Method::Head | Method::Connect => true,
|
Method::Head | Method::Connect => true,
|
||||||
_ => false })
|
_ => false }) {
|
||||||
{
|
|
||||||
// when Fetch is used only asynchronously, we will need to make sure
|
// when Fetch is used only asynchronously, we will need to make sure
|
||||||
// that nothing tries to write to the body at this point
|
// that nothing tries to write to the body at this point
|
||||||
let mut body = internal_response.body.lock().unwrap();
|
let mut body = internal_response.body.lock().unwrap();
|
||||||
*body = ResponseBody::Empty;
|
*body = ResponseBody::Empty;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Step 18
|
internal_response.get_network_error().map(|e| e.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute deferred rebinding of response
|
||||||
|
let response = if let Some(error) = internal_error {
|
||||||
|
Response::network_error(error)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 18
|
||||||
|
let mut response_loaded = false;
|
||||||
let response = if !response.is_network_error() && *request.integrity_metadata.borrow() != "" {
|
let response = if !response.is_network_error() && *request.integrity_metadata.borrow() != "" {
|
||||||
// Substep 1
|
// Substep 1
|
||||||
wait_for_response(&response, target, done_chan);
|
wait_for_response(&response, target, done_chan);
|
||||||
|
@ -510,3 +533,94 @@ fn is_null_body_status(status: &Option<StatusCode>) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?
|
||||||
|
fn should_block_nosniff(request: &Request, response: &Response) -> bool {
|
||||||
|
/// https://fetch.spec.whatwg.org/#x-content-type-options-header
|
||||||
|
/// This is needed to parse `X-Content-Type-Options` according to spec,
|
||||||
|
/// which requires that we inspect only the first value.
|
||||||
|
///
|
||||||
|
/// A [unit-like struct](https://doc.rust-lang.org/book/structs.html#unit-like-structs)
|
||||||
|
/// is sufficient since a valid header implies that we use `nosniff`.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct XContentTypeOptions();
|
||||||
|
|
||||||
|
impl Header for XContentTypeOptions {
|
||||||
|
fn header_name() -> &'static str {
|
||||||
|
"X-Content-Type-Options"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff%3F #2
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> HyperResult<Self> {
|
||||||
|
raw.first()
|
||||||
|
.and_then(|v| str::from_utf8(v).ok())
|
||||||
|
.and_then(|s| match s.trim().to_lowercase().as_str() {
|
||||||
|
"nosniff" => Some(XContentTypeOptions()),
|
||||||
|
_ => None
|
||||||
|
})
|
||||||
|
.ok_or(Error::Header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderFormat for XContentTypeOptions {
|
||||||
|
fn fmt_header(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("nosniff")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match response.headers.get::<XContentTypeOptions>() {
|
||||||
|
None => return false, // Step 1
|
||||||
|
_ => () // Step 2 & 3 are implemented by the XContentTypeOptions struct
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 4
|
||||||
|
// Note: an invalid MIME type will produce a `None`.
|
||||||
|
let content_type_header = response.headers.get::<ContentType>();
|
||||||
|
// Step 5
|
||||||
|
let type_ = request.type_;
|
||||||
|
|
||||||
|
/// https://html.spec.whatwg.org/multipage/#scriptingLanguages
|
||||||
|
#[inline]
|
||||||
|
fn is_javascript_mime_type(mime_type: &Mime) -> bool {
|
||||||
|
let javascript_mime_types: [Mime; 16] = [
|
||||||
|
mime!(Application / ("ecmascript")),
|
||||||
|
mime!(Application / ("javascript")),
|
||||||
|
mime!(Application / ("x-ecmascript")),
|
||||||
|
mime!(Application / ("x-javascript")),
|
||||||
|
mime!(Text / ("ecmascript")),
|
||||||
|
mime!(Text / ("javascript")),
|
||||||
|
mime!(Text / ("javascript1.0")),
|
||||||
|
mime!(Text / ("javascript1.1")),
|
||||||
|
mime!(Text / ("javascript1.2")),
|
||||||
|
mime!(Text / ("javascript1.3")),
|
||||||
|
mime!(Text / ("javascript1.4")),
|
||||||
|
mime!(Text / ("javascript1.5")),
|
||||||
|
mime!(Text / ("jscript")),
|
||||||
|
mime!(Text / ("livescript")),
|
||||||
|
mime!(Text / ("x-ecmascript")),
|
||||||
|
mime!(Text / ("x-javascript")),
|
||||||
|
];
|
||||||
|
|
||||||
|
javascript_mime_types.contains(mime_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_css: Mime = mime!(Text / Css);
|
||||||
|
// Assumes str::starts_with is equivalent to mime::TopLevel
|
||||||
|
return match type_ {
|
||||||
|
// Step 6
|
||||||
|
Type::Script => {
|
||||||
|
match content_type_header {
|
||||||
|
Some(&ContentType(ref mime_type)) => !is_javascript_mime_type(&mime_type),
|
||||||
|
None => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Step 7
|
||||||
|
Type::Style => {
|
||||||
|
match content_type_header {
|
||||||
|
Some(&ContentType(ref mime_type)) => mime_type != &text_css,
|
||||||
|
None => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Step 8
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ use net::test::HttpState;
|
||||||
use net_traits::IncludeSubdomains;
|
use net_traits::IncludeSubdomains;
|
||||||
use net_traits::NetworkError;
|
use net_traits::NetworkError;
|
||||||
use net_traits::ReferrerPolicy;
|
use net_traits::ReferrerPolicy;
|
||||||
use net_traits::request::{Origin, RedirectMode, Referrer, Request, RequestMode};
|
use net_traits::request::{Origin, RedirectMode, Referrer, Request, RequestMode, Type};
|
||||||
use net_traits::response::{CacheState, Response, ResponseBody, ResponseType};
|
use net_traits::response::{CacheState, Response, ResponseBody, ResponseType};
|
||||||
use servo_config::resource_files::resources_dir_path;
|
use servo_config::resource_files::resources_dir_path;
|
||||||
use servo_url::{ImmutableOrigin, ServoUrl};
|
use servo_url::{ImmutableOrigin, ServoUrl};
|
||||||
|
@ -601,6 +601,51 @@ fn test_fetch_with_sri_sucess() {
|
||||||
let _ = server.close();
|
let _ = server.close();
|
||||||
assert_eq!(response_is_done(&response), true);
|
assert_eq!(response_is_done(&response), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `fetch` should return a network error if there is a header `X-Content-Type-Options: nosniff`
|
||||||
|
#[test]
|
||||||
|
fn test_fetch_blocked_nosniff() {
|
||||||
|
#[inline]
|
||||||
|
fn test_nosniff_request(request_type: Type,
|
||||||
|
mime: Mime,
|
||||||
|
should_error: bool) {
|
||||||
|
const MESSAGE: &'static [u8] = b"";
|
||||||
|
const HEADER: &'static str = "X-Content-Type-Options";
|
||||||
|
const VALUE: &'static [u8] = b"nosniff";
|
||||||
|
|
||||||
|
let handler = move |_: HyperRequest, mut response: HyperResponse| {
|
||||||
|
let mime_header = ContentType(mime.clone());
|
||||||
|
response.headers_mut().set(mime_header);
|
||||||
|
assert!(response.headers().has::<ContentType>());
|
||||||
|
// Add the nosniff header
|
||||||
|
response.headers_mut().set_raw(HEADER, vec![VALUE.to_vec()]);
|
||||||
|
|
||||||
|
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.type_ = request_type;
|
||||||
|
let fetch_response = fetch(request, None);
|
||||||
|
let _ = server.close();
|
||||||
|
|
||||||
|
assert_eq!(fetch_response.is_network_error(), should_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tests = vec![
|
||||||
|
(Type::Script, Mime(TopLevel::Text, SubLevel::Javascript, vec![]), false),
|
||||||
|
(Type::Script, Mime(TopLevel::Text, SubLevel::Css, vec![]), true),
|
||||||
|
(Type::Style, Mime(TopLevel::Text, SubLevel::Css, vec![]), false),
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
let (type_, mime, should_error) = test;
|
||||||
|
test_nosniff_request(type_, mime, should_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn setup_server_and_fetch(message: &'static [u8], redirect_cap: u32) -> Response {
|
fn setup_server_and_fetch(message: &'static [u8], redirect_cap: u32) -> Response {
|
||||||
let handler = move |request: HyperRequest, mut response: HyperResponse| {
|
let handler = move |request: HyperRequest, mut response: HyperResponse| {
|
||||||
let redirects = match request.uri {
|
let redirects = match request.uri {
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[importscripts.html]
|
|
||||||
type: testharness
|
|
||||||
[Test importScripts()]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
[parsing-nosniff.html]
|
|
||||||
type: testharness
|
|
||||||
[URL query: first]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: uppercase]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
[script.html]
|
|
||||||
type: testharness
|
|
||||||
[URL query: ]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=x/x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
[stylesheet.html]
|
|
||||||
type: testharness
|
|
||||||
[URL query: ]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=x/x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
[worker.html]
|
|
||||||
type: testharness
|
|
||||||
[URL query: ]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[URL query: ?type=x/x]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
[WorkerGlobalScope_importScripts_NosniffErr.htm]
|
|
||||||
type: testharness
|
|
||||||
[importScripts throws on 'nosniff' violation]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
[Worker_NosniffErr.htm]
|
|
||||||
type: testharness
|
|
||||||
expected: TIMEOUT
|
|
||||||
[ Worker with nosniff X-Content-Type-Options header ]
|
|
||||||
expected: TIMEOUT
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue