Skip to content

Commit db450a0

Browse files
committed
Add simple implementation of content-security-policy on scripts / styles
This needs a lot more hooks before it'll actually be a good implementation, but for a start it can help get some feedback on if this is the right way to go about it. Part of servo#4577
1 parent 22e3797 commit db450a0

File tree

9 files changed

+168
-3
lines changed

9 files changed

+168
-3
lines changed

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/net_traits/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ test = false
1313
doctest = false
1414

1515
[dependencies]
16+
content-security-policy = {git = "https://github.com/rust-ammonia/rust-content-security-policy"}
1617
cookie = "0.11"
1718
embedder_traits = { path = "../embedder_traits" }
1819
headers = "0.2"

components/net_traits/request.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use crate::ReferrerPolicy;
66
use crate::ResourceTimingType;
7+
use content_security_policy as csp;
78
use http::HeaderMap;
89
use hyper::Method;
910
use msg::constellation_msg::PipelineId;
@@ -50,6 +51,27 @@ impl Destination {
5051
*self == Destination::SharedWorker ||
5152
*self == Destination::Worker
5253
}
54+
pub fn to_csp_destination(&self) -> csp::Destination {
55+
match *self {
56+
Destination::None => csp::Destination::None,
57+
Destination::Audio => csp::Destination::Audio,
58+
Destination::Document => csp::Destination::Document,
59+
Destination::Embed => csp::Destination::Embed,
60+
Destination::Font => csp::Destination::Font,
61+
Destination::Image => csp::Destination::Image,
62+
Destination::Manifest => csp::Destination::Manifest,
63+
Destination::Object => csp::Destination::Object,
64+
Destination::Report => csp::Destination::Report,
65+
Destination::Script => csp::Destination::Script,
66+
Destination::ServiceWorker => csp::Destination::ServiceWorker,
67+
Destination::SharedWorker => csp::Destination::SharedWorker,
68+
Destination::Style => csp::Destination::Style,
69+
Destination::Track => csp::Destination::Track,
70+
Destination::Video => csp::Destination::Video,
71+
Destination::Worker => csp::Destination::Worker,
72+
Destination::Xslt => csp::Destination::Xslt,
73+
}
74+
}
5375
}
5476

5577
/// A request [origin](https://fetch.spec.whatwg.org/#concept-request-origin)

components/script/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ bitflags = "1.0"
3737
bluetooth_traits = {path = "../bluetooth_traits"}
3838
canvas_traits = {path = "../canvas_traits"}
3939
caseless = "0.2"
40+
content-security-policy = {git = "https://github.com/rust-ammonia/rust-content-security-policy"}
4041
cookie = "0.11"
4142
chrono = "0.4"
4243
crossbeam-channel = "0.3"

components/script/dom/bindings/trace.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ use canvas_traits::webgl::{GLFormats, GLLimits, WebGLQueryId};
5151
use canvas_traits::webgl::{WebGLBufferId, WebGLChan, WebGLContextShareMode, WebGLError};
5252
use canvas_traits::webgl::{WebGLFramebufferId, WebGLMsgSender, WebGLPipeline, WebGLProgramId};
5353
use canvas_traits::webgl::{WebGLReceiver, WebGLRenderbufferId, WebGLSLVersion, WebGLSender};
54-
use canvas_traits::webgl::{WebGLShaderId, WebGLSyncId, WebGLTextureId, WebGLVersion};
54+
use canvas_traits::webgl::{WebGLShaderId, WebGLTextureId, WebGLVersion, WebGLVertexArrayId};
55+
use content_security_policy::CspList;
5556
use crossbeam_channel::{Receiver, Sender};
5657
use cssparser::RGBA;
5758
use devtools_traits::{CSSError, TimelineMarkerType, WorkerId};
@@ -167,6 +168,8 @@ unsafe_no_jsmanaged_fields!(*mut JobQueue);
167168

168169
unsafe_no_jsmanaged_fields!(Cow<'static, str>);
169170

171+
unsafe_no_jsmanaged_fields!(CspList);
172+
170173
/// Trace a `JSVal`.
171174
pub fn trace_jsval(tracer: *mut JSTracer, description: &str, val: &Heap<JSVal>) {
172175
unsafe {

components/script/dom/document.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ use crate::stylesheet_set::StylesheetSetRef;
109109
use crate::task::TaskBox;
110110
use crate::task_source::{TaskSource, TaskSourceName};
111111
use crate::timers::OneshotTimerCallback;
112+
use content_security_policy::{self as csp, CspList};
112113
use cookie::Cookie;
113114
use devtools_traits::ScriptToDevtoolsControlMsg;
114115
use dom_struct::dom_struct;
@@ -131,6 +132,7 @@ use net_traits::request::RequestBuilder;
131132
use net_traits::response::HttpsState;
132133
use net_traits::CookieSource::NonHTTP;
133134
use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
135+
use net_traits::NetworkError;
134136
use net_traits::{FetchResponseMsg, IpcSend, ReferrerPolicy};
135137
use num_traits::ToPrimitive;
136138
use percent_encoding::percent_decode;
@@ -147,7 +149,7 @@ use servo_atoms::Atom;
147149
use servo_config::pref;
148150
use servo_media::{ClientContextId, ServoMedia};
149151
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
150-
use std::borrow::ToOwned;
152+
use std::borrow::{Cow, ToOwned};
151153
use std::cell::{Cell, Ref, RefMut};
152154
use std::collections::hash_map::Entry::{Occupied, Vacant};
153155
use std::collections::{HashMap, HashSet, VecDeque};
@@ -395,6 +397,9 @@ pub struct Document {
395397
/// where `id` needs to match any of the registered ShadowRoots
396398
/// hosting the media controls UI.
397399
media_controls: DomRefCell<HashMap<String, Dom<ShadowRoot>>>,
400+
/// https://html.spec.whatwg.org/multipage/#concept-document-csp-list
401+
#[ignore_malloc_size_of = "Defined in rust-content-security-policy"]
402+
csp_list: DomRefCell<Option<CspList>>,
398403
}
399404

400405
#[derive(JSTraceable, MallocSizeOf)]
@@ -1734,6 +1739,14 @@ impl Document {
17341739
request: RequestBuilder,
17351740
fetch_target: IpcSender<FetchResponseMsg>,
17361741
) {
1742+
if self.should_request_be_blocked_by_csp(&request) == csp::CheckResult::Blocked {
1743+
fetch_target
1744+
.send(FetchResponseMsg::ProcessResponse(Err(
1745+
NetworkError::LoadCancelled,
1746+
)))
1747+
.unwrap();
1748+
return;
1749+
}
17371750
let mut loader = self.loader.borrow_mut();
17381751
loader.fetch_async(load, request, fetch_target);
17391752
}
@@ -2784,9 +2797,60 @@ impl Document {
27842797
shadow_roots: DomRefCell::new(HashSet::new()),
27852798
shadow_roots_styles_changed: Cell::new(false),
27862799
media_controls: DomRefCell::new(HashMap::new()),
2800+
csp_list: DomRefCell::new(None),
2801+
}
2802+
}
2803+
2804+
pub fn set_csp_list(&self, csp_list: Option<CspList>) {
2805+
*self.csp_list.borrow_mut() = csp_list;
2806+
}
2807+
2808+
pub fn get_csp_list(&self) -> Option<Ref<CspList>> {
2809+
let b = self.csp_list.borrow();
2810+
if b.is_none() {
2811+
None
2812+
} else {
2813+
Some(Ref::map(b, |o| o.as_ref().unwrap()))
27872814
}
27882815
}
27892816

2817+
pub fn should_request_be_blocked_by_csp(&self, request: &RequestBuilder) -> csp::CheckResult {
2818+
let request = csp::Request {
2819+
url: request.url.clone().into_url(),
2820+
origin: request.origin.clone().into_url_origin(),
2821+
redirect_count: 0,
2822+
destination: request.destination.to_csp_destination(),
2823+
initiator: csp::Initiator::Fetch,
2824+
nonce: String::new(),
2825+
integrity_metadata: String::new(),
2826+
parser_metadata: csp::ParserMetadata::None,
2827+
};
2828+
// TODO: Instead of ignoring violations, report them.
2829+
self.get_csp_list()
2830+
.map(|c| c.should_request_be_blocked(&request).0)
2831+
.unwrap_or(csp::CheckResult::Allowed)
2832+
}
2833+
2834+
pub fn should_elements_inline_type_behavior_be_blocked(
2835+
&self,
2836+
el: &Element,
2837+
type_: csp::InlineCheckType,
2838+
source: &str,
2839+
) -> csp::CheckResult {
2840+
let element = csp::Element {
2841+
nonce: el
2842+
.get_attribute(&ns!(), &local_name!("nonce"))
2843+
.map(|attr| Cow::Owned(attr.value().to_string())),
2844+
};
2845+
// TODO: Instead of ignoring violations, report them.
2846+
self.get_csp_list()
2847+
.map(|c| {
2848+
c.should_elements_inline_type_behavior_be_blocked(&element, type_, source)
2849+
.0
2850+
})
2851+
.unwrap_or(csp::CheckResult::Allowed)
2852+
}
2853+
27902854
/// Prevent any JS or layout from running until the corresponding call to
27912855
/// `remove_script_and_layout_blocker`. Used to isolate periods in which
27922856
/// the DOM is in an unstable state and should not be exposed to arbitrary

components/script/dom/globalscope.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use crate::task_source::websocket::WebsocketTaskSource;
3838
use crate::task_source::TaskSourceName;
3939
use crate::timers::{IsInterval, OneshotTimerCallback, OneshotTimerHandle};
4040
use crate::timers::{OneshotTimers, TimerCallback};
41+
use content_security_policy::CspList;
4142
use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId};
4243
use dom_struct::dom_struct;
4344
use ipc_channel::ipc::IpcSender;
@@ -812,6 +813,13 @@ impl GlobalScope {
812813
pub fn get_user_agent(&self) -> Cow<'static, str> {
813814
self.user_agent.clone()
814815
}
816+
817+
pub fn get_csp_list(&self) -> Option<CspList> {
818+
if let Some(window) = self.downcast::<Window>() {
819+
return window.Document().get_csp_list().map(|c| c.clone());
820+
}
821+
None
822+
}
815823
}
816824

817825
fn timestamp_in_ms(time: Timespec) -> u64 {

components/script/dom/htmlscriptelement.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::dom::node::{BindContext, ChildrenMutation, CloneChildrenFlag, Node};
2626
use crate::dom::performanceresourcetiming::InitiatorType;
2727
use crate::dom::virtualmethods::VirtualMethods;
2828
use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingListener};
29+
use content_security_policy as csp;
2930
use dom_struct::dom_struct;
3031
use encoding_rs::Encoding;
3132
use html5ever::{LocalName, Prefix};
@@ -442,7 +443,15 @@ impl HTMLScriptElement {
442443

443444
// TODO: Step 12: nomodule content attribute
444445

445-
// TODO(#4577): Step 13: CSP.
446+
if !element.has_attribute(&local_name!("src")) &&
447+
doc.should_elements_inline_type_behavior_be_blocked(
448+
&element,
449+
csp::InlineCheckType::Script,
450+
&text,
451+
) == csp::CheckResult::Blocked
452+
{
453+
return;
454+
}
446455

447456
// Step 14.
448457
let for_attribute = element.get_attribute(&ns!(), &local_name!("for"));

components/script/dom/servoparser/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use crate::dom::text::Text;
3535
use crate::dom::virtualmethods::vtable_for;
3636
use crate::network_listener::PreInvoke;
3737
use crate::script_thread::ScriptThread;
38+
use content_security_policy::{self as csp, CspList};
3839
use dom_struct::dom_struct;
3940
use embedder_traits::resources::{self, Resource};
4041
use encoding_rs::Encoding;
@@ -736,6 +737,44 @@ impl FetchResponseListener for ParserContext {
736737
.and_then(|meta| meta.content_type)
737738
.map(Serde::into_inner)
738739
.map(Into::into);
740+
741+
// https://www.w3.org/TR/CSP/#initialize-document-csp
742+
// XXX TODO: DO NOT MERGE INTO MASTER YET
743+
// This only implements the non-local behavior stuff; I need to figure out how to check if a URL is local.
744+
let csp_list = metadata.as_ref().and_then(|m| {
745+
let h = if let Some(h) = m.headers.as_ref() {
746+
h
747+
} else {
748+
return None;
749+
};
750+
let mut csp = h.get_all("content-security-policy").iter();
751+
// This silently ignores the CSP if it contains invalid Unicode.
752+
// We should probably report an error somewhere.
753+
let c = if let Some(c) = csp.next().and_then(|c| c.to_str().ok()) {
754+
c
755+
} else {
756+
return None;
757+
};
758+
let mut csp_list = CspList::parse(
759+
c,
760+
csp::PolicySource::Header,
761+
csp::PolicyDisposition::Enforce,
762+
);
763+
for c in csp {
764+
let c = if let Ok(c) = c.to_str() {
765+
c
766+
} else {
767+
return None;
768+
};
769+
csp_list.append(CspList::parse(
770+
c,
771+
csp::PolicySource::Header,
772+
csp::PolicyDisposition::Enforce,
773+
));
774+
}
775+
Some(csp_list)
776+
});
777+
739778
let parser = match ScriptThread::page_headers_available(&self.id, metadata) {
740779
Some(parser) => parser,
741780
None => return,
@@ -744,6 +783,8 @@ impl FetchResponseListener for ParserContext {
744783
return;
745784
}
746785

786+
parser.document.set_csp_list(csp_list);
787+
747788
self.parser = Some(Trusted::new(&*parser));
748789

749790
match content_type {

0 commit comments

Comments
 (0)