Validating Host headers in RPC requests (#1658)

* Validating Host headers in RPC requests

* Fixing convention [ci skip]

* Remove extra indent.

[ci:skip]
This commit is contained in:
Tomasz Drwięga 2016-07-19 12:53:58 +02:00 committed by Gav Wood
parent 615fca5dc5
commit 6e79a36ef6
11 changed files with 176 additions and 60 deletions

8
Cargo.lock generated
View File

@ -276,7 +276,7 @@ dependencies = [
"ethcore-util 1.3.0", "ethcore-util 1.3.0",
"hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "hyper 0.9.4 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-http-server 5.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", "jsonrpc-http-server 6.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps 0.6.0 (git+https://github.com/ethcore/parity-ui.git)", "parity-dapps 0.6.0 (git+https://github.com/ethcore/parity-ui.git)",
@ -357,7 +357,7 @@ dependencies = [
"ethsync 1.3.0", "ethsync 1.3.0",
"json-ipc-server 0.2.4 (git+https://github.com/ethcore/json-ipc-server.git)", "json-ipc-server 0.2.4 (git+https://github.com/ethcore/json-ipc-server.git)",
"jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-http-server 5.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", "jsonrpc-http-server 6.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)",
@ -629,8 +629,8 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-http-server" name = "jsonrpc-http-server"
version = "5.1.0" version = "6.1.0"
source = "git+https://github.com/ethcore/jsonrpc-http-server.git#f16b956c61e60b3a530ad4bac82112a8974cf505" source = "git+https://github.com/ethcore/jsonrpc-http-server.git#517a0d7b8c7fd099995ce4cc93f52789e83f2cdc"
dependencies = [ dependencies = [
"hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "hyper 0.9.4 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -38,6 +38,15 @@ impl ContentHandler {
} }
} }
pub fn forbidden(content: String, mimetype: String) -> Self {
ContentHandler {
code: StatusCode::Forbidden,
content: content,
mimetype: mimetype,
write_pos: 0
}
}
pub fn not_found(content: String, mimetype: String) -> Self { pub fn not_found(content: String, mimetype: String) -> Self {
ContentHandler { ContentHandler {
code: StatusCode::NotFound, code: StatusCode::NotFound,

View File

@ -129,6 +129,7 @@ impl Server {
special.insert(router::SpecialEndpoint::Utils, apps::utils()); special.insert(router::SpecialEndpoint::Utils, apps::utils());
special special
}); });
let bind_address = format!("{}", addr);
try!(hyper::Server::http(addr)) try!(hyper::Server::http(addr))
.handle(move |_| router::Router::new( .handle(move |_| router::Router::new(
@ -136,6 +137,7 @@ impl Server {
endpoints.clone(), endpoints.clone(),
special.clone(), special.clone(),
authorization.clone(), authorization.clone(),
bind_address.clone(),
)) ))
.map(|(l, srv)| { .map(|(l, srv)| {

View File

@ -0,0 +1,42 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use DAPPS_DOMAIN;
use hyper::server;
use hyper::net::HttpStream;
use jsonrpc_http_server::{is_host_header_valid};
use handlers::ContentHandler;
pub fn is_valid(request: &server::Request<HttpStream>, bind_address: &str, endpoints: Vec<String>) -> bool {
let mut endpoints = endpoints.into_iter()
.map(|endpoint| format!("{}{}", endpoint, DAPPS_DOMAIN))
.collect::<Vec<String>>();
endpoints.push(bind_address.into());
is_host_header_valid(request, &endpoints)
}
pub fn host_invalid_response() -> Box<server::Handler<HttpStream> + Send> {
Box::new(ContentHandler::forbidden(
r#"
<h1>Request with disallowed <code>Host</code> header has been blocked.</h1>
<p>Check the URL in your browser address bar.</p>
"#.into(),
"text/html".into()
))
}

View File

@ -18,6 +18,7 @@
//! Processes request handling authorization and dispatching it to proper application. //! Processes request handling authorization and dispatching it to proper application.
pub mod auth; pub mod auth;
mod host_validation;
use DAPPS_DOMAIN; use DAPPS_DOMAIN;
use std::sync::Arc; use std::sync::Arc;
@ -44,23 +45,31 @@ pub struct Router<A: Authorization + 'static> {
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>, special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>,
authorization: Arc<A>, authorization: Arc<A>,
bind_address: String,
handler: Box<server::Handler<HttpStream> + Send>, handler: Box<server::Handler<HttpStream> + Send>,
} }
impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> { impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
fn on_request(&mut self, req: server::Request<HttpStream>) -> Next { fn on_request(&mut self, req: server::Request<HttpStream>) -> Next {
// Validate Host header
if !host_validation::is_valid(&req, &self.bind_address, self.endpoints.keys().cloned().collect()) {
self.handler = host_validation::host_invalid_response();
return self.handler.on_request(req);
}
// Check authorization // Check authorization
let auth = self.authorization.is_authorized(&req); let auth = self.authorization.is_authorized(&req);
if let Authorized::No(handler) = auth {
self.handler = handler;
return self.handler.on_request(req);
}
// Choose proper handler depending on path / domain // Choose proper handler depending on path / domain
self.handler = match auth {
Authorized::No(handler) => handler,
Authorized::Yes => {
let url = extract_url(&req); let url = extract_url(&req);
let endpoint = extract_endpoint(&url); let endpoint = extract_endpoint(&url);
match endpoint { self.handler = match endpoint {
// First check special endpoints // First check special endpoints
(ref path, ref endpoint) if self.special.contains_key(endpoint) => { (ref path, ref endpoint) if self.special.contains_key(endpoint) => {
self.special.get(endpoint).unwrap().to_handler(path.clone().unwrap_or_default()) self.special.get(endpoint).unwrap().to_handler(path.clone().unwrap_or_default())
@ -77,8 +86,6 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
_ => { _ => {
self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()) self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default())
} }
}
}
}; };
// Delegate on_request to proper handler // Delegate on_request to proper handler
@ -106,7 +113,9 @@ impl<A: Authorization> Router<A> {
main_page: &'static str, main_page: &'static str,
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>, special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>,
authorization: Arc<A>) -> Self { authorization: Arc<A>,
bind_address: String,
) -> Self {
let handler = special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()); let handler = special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default());
Router { Router {
@ -114,6 +123,7 @@ impl<A: Authorization> Router<A> {
endpoints: endpoints, endpoints: endpoints,
special: special, special: special,
authorization: authorization, authorization: authorization,
bind_address: bind_address,
handler: handler, handler: handler,
} }
} }

View File

@ -23,19 +23,22 @@ pub fn rpc(handler: Arc<IoHandler>, panic_handler: Arc<Mutex<Option<Box<Fn() ->
Box::new(RpcEndpoint { Box::new(RpcEndpoint {
handler: handler, handler: handler,
panic_handler: panic_handler, panic_handler: panic_handler,
cors_domain: vec![AccessControlAllowOrigin::Null], cors_domain: Some(vec![AccessControlAllowOrigin::Null]),
// NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router.
allowed_hosts: None,
}) })
} }
struct RpcEndpoint { struct RpcEndpoint {
handler: Arc<IoHandler>, handler: Arc<IoHandler>,
panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>, panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>,
cors_domain: Vec<AccessControlAllowOrigin>, cors_domain: Option<Vec<AccessControlAllowOrigin>>,
allowed_hosts: Option<Vec<String>>,
} }
impl Endpoint for RpcEndpoint { impl Endpoint for RpcEndpoint {
fn to_handler(&self, _path: EndpointPath) -> Box<Handler> { fn to_handler(&self, _path: EndpointPath) -> Box<Handler> {
let panic_handler = PanicHandler { handler: self.panic_handler.clone() }; let panic_handler = PanicHandler { handler: self.panic_handler.clone() };
Box::new(ServerHandler::new(self.handler.clone(), self.cors_domain.clone(), panic_handler)) Box::new(ServerHandler::new(self.handler.clone(), self.cors_domain.clone(), self.allowed_hosts.clone(), panic_handler))
} }
} }

View File

@ -107,6 +107,11 @@ API and Console Options:
name. Possible name are web3, eth, net, personal, name. Possible name are web3, eth, net, personal,
ethcore, ethcore_set, traces. ethcore, ethcore_set, traces.
[default: web3,eth,net,ethcore,personal,traces]. [default: web3,eth,net,ethcore,personal,traces].
--jsonrpc-hosts HOSTS List of allowed Host header values. This option will
validate the Host header sent by the browser, it
is additional security against some attack
vectors. Special options: "all", "none",
[default: none].
--no-ipc Disable JSON-RPC over IPC service. --no-ipc Disable JSON-RPC over IPC service.
--ipc-path PATH Specify custom path for JSON-RPC over IPC service --ipc-path PATH Specify custom path for JSON-RPC over IPC service
@ -118,8 +123,8 @@ API and Console Options:
--dapps-port PORT Specify the port portion of the Dapps server --dapps-port PORT Specify the port portion of the Dapps server
[default: 8080]. [default: 8080].
--dapps-interface IP Specify the hostname portion of the Dapps --dapps-interface IP Specify the hostname portion of the Dapps
server, IP should be an interface's IP address, or server, IP should be an interface's hostname / IP
all (all interfaces) or local [default: local]. or local (localhost) [default: local].
--dapps-user USERNAME Specify username for Dapps server. It will be --dapps-user USERNAME Specify username for Dapps server. It will be
used in HTTP Basic Authentication Scheme. used in HTTP Basic Authentication Scheme.
If --dapps-pass is not specified you will be If --dapps-pass is not specified you will be
@ -311,6 +316,7 @@ pub struct Args {
pub flag_jsonrpc_interface: String, pub flag_jsonrpc_interface: String,
pub flag_jsonrpc_port: u16, pub flag_jsonrpc_port: u16,
pub flag_jsonrpc_cors: Option<String>, pub flag_jsonrpc_cors: Option<String>,
pub flag_jsonrpc_hosts: String,
pub flag_jsonrpc_apis: String, pub flag_jsonrpc_apis: String,
pub flag_no_ipc: bool, pub flag_no_ipc: bool,
pub flag_ipc_path: String, pub flag_ipc_path: String,

View File

@ -424,9 +424,22 @@ impl Configuration {
self.args.flag_rpcapi.clone().unwrap_or(self.args.flag_jsonrpc_apis.clone()) self.args.flag_rpcapi.clone().unwrap_or(self.args.flag_jsonrpc_apis.clone())
} }
pub fn rpc_cors(&self) -> Vec<String> { pub fn rpc_cors(&self) -> Option<Vec<String>> {
let cors = self.args.flag_jsonrpc_cors.clone().or(self.args.flag_rpccorsdomain.clone()); let cors = self.args.flag_jsonrpc_cors.clone().or(self.args.flag_rpccorsdomain.clone());
cors.map_or_else(Vec::new, |c| c.split(',').map(|s| s.to_owned()).collect()) cors.map(|c| c.split(',').map(|s| s.to_owned()).collect())
}
pub fn rpc_hosts(&self) -> Option<Vec<String>> {
let hosts = self.args.flag_jsonrpc_hosts.split(',').collect::<Vec<&str>>();
// look for special values
for h in &hosts {
match *h {
"none" => return Some(Vec::new()),
"all" => return None,
_ => {},
}
}
Some(hosts.into_iter().map(|h| h.into()).collect())
} }
fn geth_ipc_path(&self) -> String { fn geth_ipc_path(&self) -> String {
@ -541,8 +554,7 @@ impl Configuration {
pub fn dapps_interface(&self) -> String { pub fn dapps_interface(&self) -> String {
match self.args.flag_dapps_interface.as_str() { match self.args.flag_dapps_interface.as_str() {
"all" => "0.0.0.0", "local" => "localhost",
"local" => "127.0.0.1",
x => x, x => x,
}.into() }.into()
} }
@ -597,7 +609,7 @@ mod tests {
assert_eq!(net.rpc_enabled, true); assert_eq!(net.rpc_enabled, true);
assert_eq!(net.rpc_interface, "all".to_owned()); assert_eq!(net.rpc_interface, "all".to_owned());
assert_eq!(net.rpc_port, 8000); assert_eq!(net.rpc_port, 8000);
assert_eq!(conf.rpc_cors(), vec!["*".to_owned()]); assert_eq!(conf.rpc_cors(), Some(vec!["*".to_owned()]));
assert_eq!(conf.rpc_apis(), "web3,eth".to_owned()); assert_eq!(conf.rpc_apis(), "web3,eth".to_owned());
} }
@ -619,5 +631,22 @@ mod tests {
assert(conf1); assert(conf1);
assert(conf2); assert(conf2);
} }
#[test]
fn should_parse_rpc_hosts() {
// given
// when
let conf0 = parse(&["parity"]);
let conf1 = parse(&["parity", "--jsonrpc-hosts", "none"]);
let conf2 = parse(&["parity", "--jsonrpc-hosts", "all"]);
let conf3 = parse(&["parity", "--jsonrpc-hosts", "ethcore.io,something.io"]);
// then
assert_eq!(conf0.rpc_hosts(), Some(Vec::new()));
assert_eq!(conf1.rpc_hosts(), Some(Vec::new()));
assert_eq!(conf2.rpc_hosts(), None);
assert_eq!(conf3.rpc_hosts(), Some(vec!["ethcore.io".into(), "something.io".into()]));
}
} }

View File

@ -280,6 +280,7 @@ fn execute_client(conf: Configuration, spec: Spec, client_config: ClientConfig,
port: network_settings.rpc_port, port: network_settings.rpc_port,
apis: conf.rpc_apis(), apis: conf.rpc_apis(),
cors: conf.rpc_cors(), cors: conf.rpc_cors(),
hosts: conf.rpc_hosts(),
}, &dependencies); }, &dependencies);
// setup ipc rpc // setup ipc rpc

View File

@ -32,7 +32,8 @@ pub struct HttpConfiguration {
pub interface: String, pub interface: String,
pub port: u16, pub port: u16,
pub apis: String, pub apis: String,
pub cors: Vec<String>, pub cors: Option<Vec<String>>,
pub hosts: Option<Vec<String>>,
} }
pub struct IpcConfiguration { pub struct IpcConfiguration {
@ -66,7 +67,7 @@ pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Option<RpcServe
let url = format!("{}:{}", conf.interface, conf.port); let url = format!("{}:{}", conf.interface, conf.port);
let addr = SocketAddr::from_str(&url).unwrap_or_else(|_| die!("{}: Invalid JSONRPC listen host/port given.", url)); let addr = SocketAddr::from_str(&url).unwrap_or_else(|_| die!("{}: Invalid JSONRPC listen host/port given.", url));
Some(setup_http_rpc_server(deps, &addr, conf.cors, apis)) Some(setup_http_rpc_server(deps, &addr, conf.cors, conf.hosts, apis))
} }
fn setup_rpc_server(apis: Vec<&str>, deps: &Dependencies) -> Server { fn setup_rpc_server(apis: Vec<&str>, deps: &Dependencies) -> Server {
@ -78,21 +79,17 @@ fn setup_rpc_server(apis: Vec<&str>, deps: &Dependencies) -> Server {
pub fn setup_http_rpc_server( pub fn setup_http_rpc_server(
dependencies: &Dependencies, dependencies: &Dependencies,
url: &SocketAddr, url: &SocketAddr,
cors_domains: Vec<String>, cors_domains: Option<Vec<String>>,
allowed_hosts: Option<Vec<String>>,
apis: Vec<&str>, apis: Vec<&str>,
) -> RpcServer { ) -> RpcServer {
let server = setup_rpc_server(apis, dependencies); let server = setup_rpc_server(apis, dependencies);
let start_result = server.start_http(url, cors_domains);
let ph = dependencies.panic_handler.clone(); let ph = dependencies.panic_handler.clone();
let start_result = server.start_http(url, cors_domains, allowed_hosts, ph);
match start_result { match start_result {
Err(RpcServerError::IoError(err)) => die_with_io_error("RPC", err), Err(RpcServerError::IoError(err)) => die_with_io_error("RPC", err),
Err(e) => die!("RPC: {:?}", e), Err(e) => die!("RPC: {:?}", e),
Ok(server) => { Ok(server) => server,
server.set_panic_handler(move || {
ph.notify_all("Panic in RPC thread.".to_owned());
});
server
},
} }
} }

View File

@ -41,9 +41,10 @@ extern crate ethcore_devtools as devtools;
use std::sync::Arc; use std::sync::Arc;
use std::net::SocketAddr; use std::net::SocketAddr;
use util::panics::PanicHandler;
use self::jsonrpc_core::{IoHandler, IoDelegate}; use self::jsonrpc_core::{IoHandler, IoDelegate};
pub use jsonrpc_http_server::{Server, RpcServerError}; pub use jsonrpc_http_server::{ServerBuilder, Server, RpcServerError};
pub mod v1; pub mod v1;
pub use v1::{SigningQueue, ConfirmationsQueue}; pub use v1::{SigningQueue, ConfirmationsQueue};
@ -74,15 +75,31 @@ impl RpcServer {
} }
/// Start http server asynchronously and returns result with `Server` handle on success or an error. /// Start http server asynchronously and returns result with `Server` handle on success or an error.
pub fn start_http(&self, addr: &SocketAddr, cors_domains: Vec<String>) -> Result<Server, RpcServerError> { pub fn start_http(
let cors_domains = cors_domains.into_iter() &self,
addr: &SocketAddr,
cors_domains: Option<Vec<String>>,
allowed_hosts: Option<Vec<String>>,
panic_handler: Arc<PanicHandler>,
) -> Result<Server, RpcServerError> {
let cors_domains = cors_domains.map(|domains| {
domains.into_iter()
.map(|v| match v.as_str() { .map(|v| match v.as_str() {
"*" => jsonrpc_http_server::AccessControlAllowOrigin::Any, "*" => jsonrpc_http_server::AccessControlAllowOrigin::Any,
"null" => jsonrpc_http_server::AccessControlAllowOrigin::Null, "null" => jsonrpc_http_server::AccessControlAllowOrigin::Null,
v => jsonrpc_http_server::AccessControlAllowOrigin::Value(v.into()), v => jsonrpc_http_server::AccessControlAllowOrigin::Value(v.into()),
}) })
.collect(); .collect()
Server::start(addr, self.handler.clone(), cors_domains) });
ServerBuilder::new(self.handler.clone())
.cors(cors_domains.into())
.allowed_hosts(allowed_hosts.into())
.panic_handler(move || {
panic_handler.notify_all("Panic in RPC thread.".to_owned());
})
.start_http(addr)
} }
/// Start ipc server asynchronously and returns result with `Server` handle on success or an error. /// Start ipc server asynchronously and returns result with `Server` handle on success or an error.