Host validation (again) (#1666)

* Revert "Revert "Validating Host headers in RPC requests" (#1663)"

This reverts commit 3cc3dbef66.

* Fixing binding on MacOS
This commit is contained in:
Tomasz Drwięga 2016-07-20 12:34:17 +02:00 committed by Gav Wood
parent 9c8b80f998
commit 0cba70fba3
11 changed files with 172 additions and 56 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,45 @@
// 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>>();
// Add localhost domain as valid too if listening on loopback interface.
endpoints.push(bind_address.replace("127.0.0.1", "localhost").into());
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,40 +45,46 @@ 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 { let url = extract_url(&req);
Authorized::No(handler) => handler, let endpoint = extract_endpoint(&url);
Authorized::Yes => {
let url = extract_url(&req);
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())
}, },
// Then delegate to dapp // Then delegate to dapp
(Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => { (Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => {
self.endpoints.get(&path.app_id).unwrap().to_handler(path.clone()) self.endpoints.get(&path.app_id).unwrap().to_handler(path.clone())
}, },
// Redirection to main page // Redirection to main page
_ if *req.method() == hyper::method::Method::Get => { _ if *req.method() == hyper::method::Method::Get => {
Redirection::new(self.main_page) Redirection::new(self.main_page)
}, },
// RPC by default // RPC by default
_ => { _ => {
self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()) self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default())
}
}
} }
}; };
@ -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 IP address,
all (all interfaces) or local [default: local]. or local [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,19 @@ 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>> {
match self.args.flag_jsonrpc_hosts.as_ref() {
"none" => return Some(Vec::new()),
"all" => return None,
_ => {}
}
let hosts = self.args.flag_jsonrpc_hosts.split(',').map(|h| h.into()).collect();
Some(hosts)
} }
fn geth_ipc_path(&self) -> String { fn geth_ipc_path(&self) -> String {
@ -541,7 +551,6 @@ 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" => "127.0.0.1", "local" => "127.0.0.1",
x => x, x => x,
}.into() }.into()
@ -597,7 +606,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 +628,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,
.map(|v| match v.as_str() { addr: &SocketAddr,
"*" => jsonrpc_http_server::AccessControlAllowOrigin::Any, cors_domains: Option<Vec<String>>,
"null" => jsonrpc_http_server::AccessControlAllowOrigin::Null, allowed_hosts: Option<Vec<String>>,
v => jsonrpc_http_server::AccessControlAllowOrigin::Value(v.into()), panic_handler: Arc<PanicHandler>,
) -> Result<Server, RpcServerError> {
let cors_domains = cors_domains.map(|domains| {
domains.into_iter()
.map(|v| match v.as_str() {
"*" => jsonrpc_http_server::AccessControlAllowOrigin::Any,
"null" => jsonrpc_http_server::AccessControlAllowOrigin::Null,
v => jsonrpc_http_server::AccessControlAllowOrigin::Value(v.into()),
})
.collect()
});
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());
}) })
.collect(); .start_http(addr)
Server::start(addr, self.handler.clone(), cors_domains)
} }
/// 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.