2018-09-06 20:09:46 +00:00
|
|
|
use csrf::{AesGcmCsrfProtection, CsrfProtection, CSRF_COOKIE_NAME, CSRF_FORM_FIELD};
|
2018-06-30 10:15:06 +00:00
|
|
|
use data_encoding::{BASE64, BASE64URL_NOPAD};
|
|
|
|
use rand::prelude::thread_rng;
|
|
|
|
use rand::Rng;
|
|
|
|
use rocket::fairing::{Fairing, Info, Kind};
|
2018-09-07 21:52:36 +00:00
|
|
|
use rocket::http::uri::{Uri, Origin};
|
2018-06-30 10:15:06 +00:00
|
|
|
use rocket::http::Method::{self, *};
|
|
|
|
use rocket::outcome::Outcome;
|
|
|
|
use rocket::response::Body::Sized;
|
|
|
|
use rocket::{Data, Request, Response, Rocket, State};
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::env;
|
|
|
|
use std::io::{Cursor, Read};
|
|
|
|
use std::str::from_utf8;
|
|
|
|
|
|
|
|
use csrf_proxy::CsrfProxy;
|
|
|
|
use csrf_token::CsrfToken;
|
|
|
|
use path::Path;
|
|
|
|
use utils::parse_args;
|
|
|
|
|
2018-09-06 20:09:46 +00:00
|
|
|
const CSRF_FORM_FIELD_MULTIPART: &[u8] =
|
|
|
|
"Content-Disposition: form-data; name=\"csrf-token\"".as_bytes();
|
2018-06-30 10:15:06 +00:00
|
|
|
|
|
|
|
/// Builder for [CsrfFairing](struct.CsrfFairing.html)
|
|
|
|
///
|
|
|
|
/// The `CsrfFairingBuilder` type allows for creation and configuration of a [CsrfFairing](struct.CsrfFairing.html), the
|
|
|
|
/// main struct of this crate.
|
|
|
|
///
|
|
|
|
/// # Usage
|
|
|
|
/// A Builder is created via the [`new`] method. Then you can configure it with others provided
|
|
|
|
/// methods, and get a [CsrfFairing](struct.CsrfFairing.html) by a call to [`finalize`]
|
|
|
|
///
|
|
|
|
/// [`new`]: #method.new
|
|
|
|
/// [`finalize`]: #method.finalize
|
|
|
|
///
|
|
|
|
/// ## Examples
|
|
|
|
///
|
|
|
|
/// The following shippet show 'CsrfFairingBuilder' being used to create a fairing protecting all
|
|
|
|
/// endpoints and redirecting error to `/csrf-violation` and treat them as if they where `GET`
|
|
|
|
/// request then.
|
|
|
|
///
|
2018-06-30 11:06:28 +00:00
|
|
|
/// ```rust,no_run
|
|
|
|
/// # extern crate rocket;
|
|
|
|
/// # extern crate rocket_csrf;
|
2018-06-30 10:15:06 +00:00
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
2018-06-30 11:06:28 +00:00
|
|
|
/// # use rocket::Rocket;
|
|
|
|
///
|
|
|
|
/// # fn main() {
|
|
|
|
/// Rocket::ignite()
|
|
|
|
/// .attach(CsrfFairingBuilder::new()
|
|
|
|
/// .set_default_target("/csrf-violation".to_owned(), rocket::http::Method::Get)
|
|
|
|
/// .finalize().unwrap())
|
2018-06-30 10:15:06 +00:00
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
2018-06-30 11:06:28 +00:00
|
|
|
/// # }
|
2018-06-30 10:15:06 +00:00
|
|
|
/// ```
|
|
|
|
|
|
|
|
pub struct CsrfFairingBuilder {
|
|
|
|
duration: i64,
|
|
|
|
default_target: (String, Method),
|
|
|
|
exceptions: Vec<(String, String, Method)>,
|
|
|
|
secret: Option<[u8; 32]>,
|
|
|
|
auto_insert: bool,
|
|
|
|
auto_insert_disable_prefix: Vec<String>,
|
|
|
|
auto_insert_max_size: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CsrfFairingBuilder {
|
|
|
|
/// Create a new builder with default values.
|
|
|
|
pub fn new() -> Self {
|
|
|
|
CsrfFairingBuilder {
|
|
|
|
duration: 60 * 60,
|
|
|
|
default_target: (String::from("/"), Get),
|
|
|
|
exceptions: Vec::new(),
|
|
|
|
secret: None,
|
|
|
|
auto_insert: true,
|
|
|
|
auto_insert_disable_prefix: Vec::new(),
|
|
|
|
auto_insert_max_size: 16 * 1024,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the timeout (in seconds) of CSRF tokens generated by the final Fairing. Default timeout
|
|
|
|
/// is one hour.
|
|
|
|
pub fn set_timeout(mut self, timeout: i64) -> Self {
|
|
|
|
self.duration = timeout;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the default route when an invalide request is catched, you may add a <uri> as a segment
|
|
|
|
/// or a param to get the percent-encoded original target. You can also set the method of the
|
|
|
|
/// route to which you choosed to redirect.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
2018-06-30 11:06:28 +00:00
|
|
|
/// ```rust,no_run
|
|
|
|
/// # extern crate rocket;
|
|
|
|
/// # extern crate rocket_csrf;
|
2018-06-30 10:15:06 +00:00
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
2018-06-30 11:06:28 +00:00
|
|
|
/// # use rocket::Rocket;
|
|
|
|
///
|
2018-06-30 10:15:06 +00:00
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
2018-06-30 11:06:28 +00:00
|
|
|
/// .set_default_target("/csrf-violation".to_owned(), rocket::http::Method::Get)
|
|
|
|
/// .finalize().unwrap())
|
2018-06-30 10:15:06 +00:00
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
|
|
|
|
pub fn set_default_target(mut self, default_target: String, method: Method) -> Self {
|
|
|
|
self.default_target = (default_target, method);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the list of exceptions which will not be redirected to the default route, removing any
|
|
|
|
/// previously added exceptions, to juste add exceptions use [`add_exceptions`] instead. A route may
|
|
|
|
/// contain dynamic parts noted as <name>, which will be replaced in the target route.
|
|
|
|
/// Note that this is not aware of Rocket's routes, so matching `/something/<dynamic>` while
|
|
|
|
/// match against `/something/static`, even if those are different routes for Rocket. To
|
|
|
|
/// circunvence this issue, you can add a (not so) exception matching the static route before
|
|
|
|
/// the dynamic one, and redirect it to the default target manually.
|
|
|
|
///
|
|
|
|
/// [`add_exceptions`]: #method.add_exceptions
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
2018-06-30 11:06:28 +00:00
|
|
|
/// ```rust,no_run
|
|
|
|
/// # extern crate rocket;
|
|
|
|
/// # extern crate rocket_csrf;
|
2018-06-30 10:15:06 +00:00
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
2018-06-30 11:06:28 +00:00
|
|
|
/// # use rocket::Rocket;
|
|
|
|
///
|
2018-06-30 10:15:06 +00:00
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
/// .set_exceptions(vec![
|
2018-06-30 11:06:28 +00:00
|
|
|
/// ("/some/path".to_owned(), "/some/path".to_owned(), rocket::http::Method::Post),//don't verify csrf token
|
|
|
|
/// ("/some/<other>/path".to_owned(), "/csrf-error?where=<other>".to_owned(), rocket::http::Method::Get)
|
2018-06-30 10:15:06 +00:00
|
|
|
/// ])
|
2018-06-30 11:06:28 +00:00
|
|
|
/// .finalize().unwrap())
|
2018-06-30 10:15:06 +00:00
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
pub fn set_exceptions(mut self, exceptions: Vec<(String, String, Method)>) -> Self {
|
|
|
|
self.exceptions = exceptions;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
/// Add the to list of exceptions which will not be redirected to the default route. See
|
|
|
|
/// [`set_exceptions`] for more informations on how exceptions work.
|
|
|
|
///
|
|
|
|
/// [`set_exceptions`]: #method.set_exceptions
|
|
|
|
pub fn add_exceptions(mut self, exceptions: Vec<(String, String, Method)>) -> Self {
|
|
|
|
self.exceptions.extend(exceptions);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the secret key used to generate secure cryptographic tokens. If not set, rocket_csrf
|
|
|
|
/// will attempt to get the secret used by Rocket for it's own private cookies via the
|
|
|
|
/// ROCKET_SECRET_KEY environment variable, or will generate a new one at each restart.
|
|
|
|
/// Having the secret key set (via this or Rocket environment variable) allow tokens to keep
|
|
|
|
/// their validity in case of an application restart.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
2018-06-30 11:06:28 +00:00
|
|
|
/// ```rust,no_run
|
|
|
|
/// # extern crate rocket;
|
|
|
|
/// # extern crate rocket_csrf;
|
2018-06-30 10:15:06 +00:00
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
2018-06-30 11:06:28 +00:00
|
|
|
/// # use rocket::Rocket;
|
|
|
|
///
|
2018-06-30 10:15:06 +00:00
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
/// .set_secret([0;32])//don't do this, use trully secret array instead
|
2018-06-30 11:06:28 +00:00
|
|
|
/// .finalize().unwrap())
|
2018-06-30 10:15:06 +00:00
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
pub fn set_secret(mut self, secret: [u8; 32]) -> Self {
|
|
|
|
self.secret = Some(secret);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set if this should modify response to insert tokens automatically in all forms. If true,
|
|
|
|
/// this will insert tokens in all forms it encounter, if false, you will have to add them via
|
|
|
|
/// [CsrfFairing](struct.CsrfFairing.html), which you may obtain via request guards.
|
|
|
|
///
|
|
|
|
pub fn set_auto_insert(mut self, auto_insert: bool) -> Self {
|
|
|
|
self.auto_insert = auto_insert;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set prefixs for which this will not try to add tokens in forms. This has no effect if
|
|
|
|
/// auto_insert is set to false. Not having to parse response on paths witch don't need it may
|
|
|
|
/// improve performances, but not that only html documents are parsed, so it's not usefull to
|
|
|
|
/// use it on routes containing only images or stillsheets.
|
|
|
|
pub fn set_auto_insert_disable_prefix(mut self, auto_insert_prefix: Vec<String>) -> Self {
|
|
|
|
self.auto_insert_disable_prefix = auto_insert_prefix;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the maximum size of a request before it get send chunked. A request will need at most
|
|
|
|
/// this additional memory for the buffer used to parse and tokens into forms. This have no
|
|
|
|
/// effect if auto_insert is set to false. Default value is 16Kio
|
|
|
|
pub fn set_auto_insert_max_chunk_size(mut self, chunk_size: u64) -> Self {
|
|
|
|
self.auto_insert_max_size = chunk_size;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the fairing from the builder.
|
|
|
|
pub fn finalize(self) -> Result<CsrfFairing, ()> {
|
|
|
|
let secret = self.secret.unwrap_or_else(|| {
|
|
|
|
//use provided secret if one is
|
|
|
|
env::vars()
|
|
|
|
.find(|(key, _)| key == "ROCKET_SECRET_KEY")
|
|
|
|
.and_then(|(_, value)| {
|
|
|
|
let b64 = BASE64.decode(value.as_bytes());
|
|
|
|
if let Ok(b64) = b64 {
|
|
|
|
if b64.len() == 32 {
|
|
|
|
let mut array = [0; 32];
|
|
|
|
array.copy_from_slice(&b64);
|
|
|
|
Some(array)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})//else get secret environment variable
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
eprintln!("[rocket_csrf] No secret key was found, you should consider set one to keep token validity across application restart");
|
|
|
|
thread_rng().gen()
|
|
|
|
}) //if environment variable is not set, generate a random secret and print a warning
|
|
|
|
});
|
|
|
|
|
|
|
|
let default_target = Path::from(&self.default_target.0);
|
|
|
|
let mut hashmap = HashMap::new();
|
2018-07-11 17:39:36 +00:00
|
|
|
hashmap.insert("uri", "".to_owned());
|
2018-06-30 10:15:06 +00:00
|
|
|
if default_target.map(&hashmap).is_none() {
|
|
|
|
return Err(());
|
|
|
|
} //verify if this path is valid as default path, i.e. it have at most one dynamic part which is <uri>
|
|
|
|
Ok(CsrfFairing {
|
|
|
|
duration: self.duration,
|
|
|
|
default_target: (default_target, self.default_target.1),
|
|
|
|
exceptions: self
|
|
|
|
.exceptions
|
|
|
|
.iter()
|
|
|
|
.map(|(a, b, m)| (Path::from(&a), Path::from(&b), *m))//TODO verify if source and target are compatible
|
|
|
|
.collect(),
|
|
|
|
secret,
|
|
|
|
auto_insert: self.auto_insert,
|
|
|
|
auto_insert_disable_prefix: self.auto_insert_disable_prefix,
|
|
|
|
auto_insert_max_size: self.auto_insert_max_size,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for CsrfFairingBuilder {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Fairing to protect against Csrf attacks.
|
|
|
|
///
|
|
|
|
/// The `CsrfFairing` type protect a Rocket instance against Csrf attack by requesting mendatory
|
|
|
|
/// token on any POST, PUT, DELETE or PATCH request.
|
|
|
|
/// This is created via a [CsrfFairingBuilder](struct.CsrfFairingBuilder.html), and implement nothing else than the `Fairing` trait.
|
|
|
|
///
|
|
|
|
/// [`CsrfFairingBuilder`]: /rocket_csrf/struct.CsrfFairing.html
|
|
|
|
pub struct CsrfFairing {
|
|
|
|
duration: i64,
|
|
|
|
default_target: (Path, Method),
|
|
|
|
exceptions: Vec<(Path, Path, Method)>,
|
|
|
|
secret: [u8; 32],
|
|
|
|
auto_insert: bool,
|
|
|
|
auto_insert_disable_prefix: Vec<String>,
|
|
|
|
auto_insert_max_size: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Fairing for CsrfFairing {
|
|
|
|
fn info(&self) -> Info {
|
|
|
|
if self.auto_insert {
|
|
|
|
Info {
|
|
|
|
name: "CSRF protection",
|
|
|
|
kind: Kind::Attach | Kind::Request | Kind::Response,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Info {
|
|
|
|
name: "CSRF protection",
|
|
|
|
kind: Kind::Attach | Kind::Request,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
|
|
|
|
Ok(rocket.manage((AesGcmCsrfProtection::from_key(self.secret), self.duration))) //add the Csrf engine to Rocket's managed state
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_request(&self, request: &mut Request, data: &Data) {
|
|
|
|
match request.method() {
|
|
|
|
Get | Head | Connect | Options => {
|
|
|
|
let _ = request.guard::<CsrfToken>(); //force regeneration of csrf cookies
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
};
|
|
|
|
|
|
|
|
let (csrf_engine, _) = request
|
|
|
|
.guard::<State<(AesGcmCsrfProtection, i64)>>()
|
|
|
|
.unwrap()
|
|
|
|
.inner();
|
|
|
|
|
|
|
|
let cookie = request
|
|
|
|
.cookies()
|
|
|
|
.get(CSRF_COOKIE_NAME)
|
2018-09-08 13:01:39 +00:00
|
|
|
.and_then(|cookie| BASE64URL_NOPAD.decode(cookie.value().as_bytes()).ok())
|
2018-06-30 10:15:06 +00:00
|
|
|
.and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok()); //get and parse Csrf cookie
|
|
|
|
|
|
|
|
let _ = request.guard::<CsrfToken>(); //force regeneration of csrf cookies
|
|
|
|
|
2018-09-06 20:09:46 +00:00
|
|
|
let token = if request
|
|
|
|
.content_type()
|
|
|
|
.map(|c| c.media_type())
|
|
|
|
.filter(|m| m.top() == "multipart" && m.sub() == "form-data")
|
|
|
|
.is_some()
|
|
|
|
{
|
2018-09-03 07:10:29 +00:00
|
|
|
data.peek().split(|&c| c==0x0A || c==0x0D)//0x0A=='\n', 0x0D=='\r'
|
|
|
|
.filter(|l| l.len() > 0)
|
|
|
|
.skip_while(|&l| l != CSRF_FORM_FIELD_MULTIPART && l != &CSRF_FORM_FIELD_MULTIPART[..CSRF_FORM_FIELD_MULTIPART.len()-2])
|
|
|
|
.skip(1)
|
|
|
|
.map(|token| token.split(|&c| c==10 || c==13).next())
|
|
|
|
.next().unwrap_or(None)
|
|
|
|
} else {
|
|
|
|
parse_args(from_utf8(data.peek()).unwrap_or(""))
|
2018-09-06 20:09:46 +00:00
|
|
|
.filter_map(|(key, token)| {
|
|
|
|
if key == CSRF_FORM_FIELD {
|
|
|
|
Some(token.as_bytes())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
2018-09-03 07:10:29 +00:00
|
|
|
.next()
|
|
|
|
}.and_then(|token| BASE64URL_NOPAD.decode(&token).ok())
|
|
|
|
.and_then(|token| csrf_engine.parse_token(&token).ok());
|
2018-07-20 15:10:20 +00:00
|
|
|
|
2018-06-30 10:15:06 +00:00
|
|
|
if let Some(token) = token {
|
|
|
|
if let Some(cookie) = cookie {
|
|
|
|
if csrf_engine.verify_token_pair(&token, &cookie) {
|
|
|
|
return; //if we got both token and cookie, and they match each other, we do nothing
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//Request reaching here are violating Csrf protection
|
|
|
|
|
|
|
|
for (src, dst, method) in &self.exceptions {
|
|
|
|
if let Some(param) = src.extract(&request.uri().to_string()) {
|
|
|
|
if let Some(destination) = dst.map(¶m) {
|
2018-09-07 21:52:36 +00:00
|
|
|
if let Ok(origin) = Origin::parse_owned(destination) {
|
|
|
|
request.set_uri(origin);
|
|
|
|
request.set_method(*method);
|
|
|
|
return;
|
|
|
|
}
|
2018-06-30 10:15:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//if request matched no exception, reroute it to default target
|
|
|
|
|
|
|
|
let uri = request.uri().to_string();
|
|
|
|
let uri = Uri::percent_encode(&uri);
|
2018-07-11 17:39:36 +00:00
|
|
|
let mut param: HashMap<&str, String> = HashMap::new();
|
|
|
|
param.insert("uri", uri.to_string());
|
2018-09-07 21:52:36 +00:00
|
|
|
let destination = self.default_target.0.map(¶m).unwrap();
|
|
|
|
let origin = Origin::parse_owned(destination).unwrap();
|
|
|
|
|
|
|
|
request.set_uri(origin);
|
2018-06-30 10:15:06 +00:00
|
|
|
request.set_method(self.default_target.1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_response<'a>(&self, request: &Request, response: &mut Response<'a>) {
|
|
|
|
if let Some(ct) = response.content_type() {
|
|
|
|
if !ct.is_html() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} //if content type is not html, we do nothing
|
|
|
|
|
|
|
|
let uri = request.uri().to_string();
|
|
|
|
if self
|
|
|
|
.auto_insert_disable_prefix
|
|
|
|
.iter()
|
2018-09-06 20:09:46 +00:00
|
|
|
.any(|prefix| uri.starts_with(prefix))
|
|
|
|
{
|
2018-06-30 10:15:06 +00:00
|
|
|
return;
|
|
|
|
} //if request is on an ignored prefix, ignore it
|
|
|
|
|
|
|
|
let token = match request.guard::<CsrfToken>() {
|
|
|
|
Outcome::Success(t) => t,
|
|
|
|
_ => return,
|
|
|
|
}; //if we can't get a token, leave request unchanged, we can't do anything anyway
|
|
|
|
|
|
|
|
let body = response.take_body(); //take request body from Rocket
|
|
|
|
if body.is_none() {
|
|
|
|
return;
|
|
|
|
} //if there was no body, leave it that way
|
|
|
|
let body = body.unwrap();
|
|
|
|
|
|
|
|
if let Sized(body_reader, len) = body {
|
|
|
|
if len <= self.auto_insert_max_size {
|
|
|
|
//if this is a small enought body, process the full body
|
|
|
|
let mut res = Vec::with_capacity(len as usize);
|
2018-08-08 19:21:00 +00:00
|
|
|
CsrfProxy::from(body_reader, &token.value())
|
2018-06-30 10:15:06 +00:00
|
|
|
.read_to_end(&mut res)
|
|
|
|
.unwrap();
|
|
|
|
response.set_sized_body(Cursor::new(res));
|
|
|
|
} else {
|
|
|
|
//if body is of known but long size, change it to a stream to preserve memory, by encapsulating it into our "proxy" struct
|
|
|
|
let body = body_reader;
|
2018-08-08 19:21:00 +00:00
|
|
|
response.set_streamed_body(Box::new(CsrfProxy::from(body, &token.value())));
|
2018-06-30 10:15:06 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
//if body is of unknown size, encapsulate it into our "proxy" struct
|
|
|
|
let body = body.into_inner();
|
2018-08-08 19:21:00 +00:00
|
|
|
response.set_streamed_body(Box::new(CsrfProxy::from(body, &token.value())));
|
2018-06-30 10:15:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-09-08 13:01:39 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use csrf::{CSRF_COOKIE_NAME, CSRF_FORM_FIELD};
|
2018-09-08 13:03:41 +00:00
|
|
|
use rocket::{
|
|
|
|
http::{Cookie, Header, Method}, local::{Client, LocalRequest}, Rocket,
|
|
|
|
};
|
2018-09-08 13:01:39 +00:00
|
|
|
|
|
|
|
fn default_builder() -> CsrfFairingBuilder {
|
|
|
|
super::CsrfFairingBuilder::new()
|
|
|
|
.set_default_target("/csrf".to_owned(), Method::Get)
|
2018-09-08 13:03:41 +00:00
|
|
|
.set_exceptions(vec![(
|
|
|
|
"/ex1".to_owned(),
|
|
|
|
"/ex1-target".to_owned(),
|
|
|
|
Method::Post,
|
|
|
|
)])
|
|
|
|
.add_exceptions(vec![(
|
|
|
|
"/ex2/<dyn>".to_owned(),
|
|
|
|
"/ex2-target/<dyn>".to_owned(),
|
|
|
|
Method::Get,
|
|
|
|
)])
|
2018-09-08 13:01:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn default_rocket(csrf_fairing: CsrfFairing) -> Rocket {
|
|
|
|
::rocket::ignite()
|
2018-09-08 13:03:41 +00:00
|
|
|
.mount(
|
|
|
|
"/",
|
|
|
|
routes![
|
|
|
|
index,
|
|
|
|
post_index,
|
|
|
|
token,
|
|
|
|
csrf,
|
|
|
|
get_ex1,
|
|
|
|
post_ex1,
|
|
|
|
target_ex1,
|
|
|
|
post_ex2,
|
|
|
|
target_ex2,
|
|
|
|
static_route
|
|
|
|
],
|
|
|
|
)
|
2018-09-08 13:01:39 +00:00
|
|
|
.attach(csrf_fairing)
|
|
|
|
}
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
fn get_token(client: &Client) -> (String, String) {
|
|
|
|
let mut response = client.get("/token").dispatch(); //get token and cookie
|
2018-09-08 13:01:39 +00:00
|
|
|
let token = response.body_string().unwrap();
|
2018-09-08 13:03:41 +00:00
|
|
|
let cookie = response
|
|
|
|
.headers()
|
|
|
|
.get("set-cookie")
|
|
|
|
.next()
|
|
|
|
.unwrap()
|
|
|
|
.split(|c| c == '=' || c == ';')
|
|
|
|
.nth(1)
|
|
|
|
.unwrap()
|
|
|
|
.to_owned();
|
2018-09-08 13:01:39 +00:00
|
|
|
(token, cookie)
|
|
|
|
}
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
fn post_token(client: &Client, path: String, token: String, cookie: String) -> LocalRequest {
|
2018-09-08 13:01:39 +00:00
|
|
|
let token = if token.len() > 0 {
|
|
|
|
let mut t = Vec::new();
|
|
|
|
t.append(&mut CSRF_FORM_FIELD.as_bytes().to_vec());
|
2018-09-08 13:03:41 +00:00
|
|
|
t.push(0x3D); //'='
|
2018-09-08 13:01:39 +00:00
|
|
|
t.append(&mut token.as_bytes().to_vec());
|
|
|
|
t
|
|
|
|
} else {
|
|
|
|
Vec::new()
|
|
|
|
};
|
2018-09-08 13:03:41 +00:00
|
|
|
let req = client.post(path).body(&token);
|
2018-09-08 13:01:39 +00:00
|
|
|
if cookie.len() > 0 {
|
|
|
|
req.cookie(Cookie::new(CSRF_COOKIE_NAME, cookie))
|
|
|
|
} else {
|
|
|
|
req
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_redirection_on_failure() {
|
|
|
|
let rocket = default_rocket(default_builder().finalize().unwrap());
|
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.post("/").dispatch(); //violation well detected
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("violation".to_owned()));
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.post("/ex1").dispatch(); //redirection on post
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("target-ex1".to_owned()));
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.post("/ex2/abcd").dispatch(); //redirection with dyn part
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("abcd".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_non_redirection() {
|
|
|
|
let rocket = default_rocket(default_builder().finalize().unwrap());
|
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.get("/ex1").dispatch(); //no redirection on get
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("get-ex1".to_owned()));
|
|
|
|
|
|
|
|
let (token, cookie) = get_token(&client);
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client, "/".to_owned(), token.clone(), cookie.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("success".to_owned()));
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client, "/ex1".to_owned(), token.clone(), cookie.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("post-ex1".to_owned()));
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = post_token(
|
|
|
|
&client,
|
|
|
|
"/ex2/some-url".to_owned(),
|
|
|
|
token.clone(),
|
|
|
|
cookie.clone(),
|
|
|
|
).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("valid-dyn-req".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_token_timeout() {
|
|
|
|
let rocket = default_rocket(default_builder().set_timeout(5).finalize().unwrap());
|
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
let (token, cookie) = get_token(&client);
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client, "/".to_owned(), token.clone(), cookie.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("success".to_owned()));
|
|
|
|
::std::thread::sleep(::std::time::Duration::from_secs(6));
|
|
|
|
|
|
|
|
//access / with timed out token
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client, "/".to_owned(), token.clone(), cookie.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("violation".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_invalid_token_pair() {
|
|
|
|
let rocket1 = default_rocket(default_builder().set_secret([0; 32]).finalize().unwrap());
|
|
|
|
let client1 = Client::new(rocket1).expect("valid rocket instance");
|
|
|
|
let rocket2 = default_rocket(default_builder().set_secret([0; 32]).finalize().unwrap());
|
|
|
|
let client2 = Client::new(rocket2).expect("valid rocket instance");
|
|
|
|
|
|
|
|
let (token, cookie) = get_token(&client1);
|
|
|
|
|
|
|
|
//having only one part fail
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client2, "/".to_owned(), token.clone(), "".to_owned()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("violation".to_owned()));
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client1, "/".to_owned(), "".to_owned(), cookie.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("violation".to_owned()));
|
|
|
|
|
|
|
|
let (token2, _cookie2) = get_token(&client2);
|
|
|
|
|
|
|
|
//having 2 incompatible parts fail
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client1, "/".to_owned(), token2.clone(), cookie.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("violation".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_multiple_parametters() {
|
|
|
|
let rocket = default_rocket(default_builder().finalize().unwrap());
|
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
let (token, cookie) = get_token(&client);
|
|
|
|
|
|
|
|
let mut body = Vec::new();
|
|
|
|
body.append(&mut "key1=value1&".as_bytes().to_vec());
|
|
|
|
body.append(&mut CSRF_FORM_FIELD.as_bytes().to_vec());
|
2018-09-08 13:03:41 +00:00
|
|
|
body.push(0x3D); //'='
|
2018-09-08 13:01:39 +00:00
|
|
|
body.append(&mut token.as_bytes().to_vec());
|
|
|
|
body.append(&mut "&key2=value2".as_bytes().to_vec());
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client
|
|
|
|
.post("/")
|
2018-09-08 13:01:39 +00:00
|
|
|
.body(body)
|
|
|
|
.cookie(Cookie::new("something", "before"))
|
|
|
|
.cookie(Cookie::new(CSRF_COOKIE_NAME, cookie))
|
2018-09-08 13:03:41 +00:00
|
|
|
.cookie(Cookie::new("and", "after"))
|
|
|
|
.dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
|
|
|
|
assert_eq!(response.body_string(), Some("success".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_multipart() {
|
|
|
|
let body_before = "-----------------------------9051914041544843365972754266
|
|
|
|
Content-Disposition: form-data; name=\"something\"
|
|
|
|
|
|
|
|
value
|
|
|
|
-----------------------------9051914041544843365972754266
|
|
|
|
Content-Disposition: form-data; name=\"";
|
|
|
|
let body_middle = "\"
|
|
|
|
|
|
|
|
";
|
|
|
|
let body_after = "
|
|
|
|
|
|
|
|
-----------------------------9051914041544843365972754266
|
|
|
|
Content-Disposition: form-data; name=\"hey\"; filename=\"whatsup\"
|
|
|
|
|
|
|
|
How are you?
|
|
|
|
|
|
|
|
-----------------------------9051914041544843365972754266--";
|
|
|
|
let rocket = default_rocket(default_builder().finalize().unwrap());
|
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
let (token, cookie) = get_token(&client);
|
|
|
|
|
|
|
|
let mut body = Vec::new();
|
|
|
|
body.append(&mut body_before.as_bytes().to_vec());
|
|
|
|
body.append(&mut CSRF_FORM_FIELD.as_bytes().to_vec());
|
|
|
|
body.append(&mut body_middle.as_bytes().to_vec());
|
|
|
|
body.append(&mut token.as_bytes().to_vec());
|
|
|
|
body.append(&mut body_after.as_bytes().to_vec());
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client
|
|
|
|
.post("/")
|
|
|
|
.header(Header::new(
|
|
|
|
"Content-Type",
|
|
|
|
"multipart/form-data; boundary=\
|
|
|
|
---------------------------\
|
|
|
|
9051914041544843365972754266",
|
|
|
|
))
|
2018-09-08 13:01:39 +00:00
|
|
|
.body(body)
|
|
|
|
.cookie(Cookie::new(CSRF_COOKIE_NAME, cookie.clone()))
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.body_string(), Some("success".to_owned()));
|
|
|
|
let mut body = Vec::new();
|
|
|
|
body.append(&mut body_before.as_bytes().to_vec());
|
|
|
|
body.append(&mut CSRF_FORM_FIELD.as_bytes().to_vec());
|
|
|
|
body.append(&mut body_middle.as_bytes().to_vec());
|
|
|
|
body.append(&mut "not_a_token".as_bytes().to_vec());
|
|
|
|
body.append(&mut body_after.as_bytes().to_vec());
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client
|
|
|
|
.post("/")
|
|
|
|
.header(Header::new(
|
|
|
|
"Content-Type",
|
|
|
|
"multipart/form-data; boundary=\
|
|
|
|
---------------------------\
|
|
|
|
9051914041544843365972754266",
|
|
|
|
))
|
2018-09-08 13:01:39 +00:00
|
|
|
.body(body)
|
|
|
|
.cookie(Cookie::new(CSRF_COOKIE_NAME, cookie))
|
|
|
|
.dispatch();
|
|
|
|
|
|
|
|
assert_eq!(response.body_string(), Some("violation".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_token_insertion() {
|
2018-09-08 13:03:41 +00:00
|
|
|
let rocket = default_rocket(
|
|
|
|
default_builder()
|
|
|
|
.set_auto_insert_disable_prefix(vec!["/static".to_owned()])
|
|
|
|
.finalize()
|
|
|
|
.unwrap(),
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.get("/").dispatch(); //token well inserted
|
|
|
|
assert!(
|
|
|
|
response.body_string().unwrap().len()
|
|
|
|
> "<div><form></form></div>".len()
|
|
|
|
+ "<input type=\"hidden\" name=\"csrf-token\" value=\"\"/>".len()
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.get("/static/something").dispatch(); //url well ignored by token inserter
|
|
|
|
assert_eq!(
|
|
|
|
response.body_string(),
|
|
|
|
Some("<div><form></form></div>".to_owned())
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_auto_insert_disabled() {
|
|
|
|
let rocket = default_rocket(default_builder().set_auto_insert(false).finalize().unwrap());
|
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
|
|
|
let mut response = client.get("/").dispatch();
|
2018-09-08 13:03:41 +00:00
|
|
|
assert_eq!(
|
|
|
|
response.body_string(),
|
|
|
|
Some("<div><form></form></div>".to_owned())
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_auto_insert_stream() {
|
2018-09-08 13:03:41 +00:00
|
|
|
let rocket = default_rocket(
|
|
|
|
default_builder()
|
|
|
|
.set_auto_insert_max_chunk_size(1)
|
|
|
|
.finalize()
|
|
|
|
.unwrap(),
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
let client = Client::new(rocket).expect("valid rocket instance");
|
|
|
|
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response = client.get("/").dispatch(); //token well inserted
|
|
|
|
assert!(
|
|
|
|
response.body_string().unwrap().len()
|
|
|
|
> "<div><form></form></div>".len()
|
|
|
|
+ "<input type=\"hidden\" name=\"csrf-token\" value=\"\"/>".len()
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
|
|
|
|
//TODO test stream body
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_key_from_env() {
|
2018-09-08 13:03:41 +00:00
|
|
|
env::set_var(
|
|
|
|
"ROCKET_SECRET_KEY",
|
|
|
|
"BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
|
|
|
|
let rocket1 = default_rocket(default_builder().finalize().unwrap());
|
|
|
|
let client1 = Client::new(rocket1).expect("valid rocket instance");
|
|
|
|
let rocket2 = default_rocket(default_builder().finalize().unwrap());
|
|
|
|
let client2 = Client::new(rocket2).expect("valid rocket instance");
|
|
|
|
|
|
|
|
let (_token, _cookie) = get_token(&client1);
|
|
|
|
let (token2, cookie2) = get_token(&client2);
|
|
|
|
|
|
|
|
//client 1 and 2 should be compatible
|
2018-09-08 13:03:41 +00:00
|
|
|
let mut response =
|
|
|
|
post_token(&client1, "/".to_owned(), token2.clone(), cookie2.clone()).dispatch();
|
2018-09-08 13:01:39 +00:00
|
|
|
assert_eq!(response.body_string(), Some("success".to_owned()));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_invalid_default_target() {
|
2018-09-08 13:03:41 +00:00
|
|
|
assert!(
|
|
|
|
default_builder()
|
|
|
|
.set_default_target("/<invalid>".to_owned(), Method::Get)
|
|
|
|
.finalize()
|
|
|
|
.is_err()
|
|
|
|
);
|
|
|
|
assert!(
|
|
|
|
default_builder()
|
|
|
|
.set_default_target("/<uri>".to_owned(), Method::Get)
|
|
|
|
.finalize()
|
|
|
|
.is_ok()
|
|
|
|
);
|
2018-09-08 13:01:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//Routes for above test
|
|
|
|
#[get("/")]
|
|
|
|
fn index() -> ::rocket::response::content::Content<&'static str> {
|
2018-09-08 13:03:41 +00:00
|
|
|
::rocket::response::content::Content(
|
|
|
|
::rocket::http::ContentType::HTML,
|
|
|
|
"<div><form></form></div>",
|
|
|
|
)
|
2018-09-08 13:01:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/")]
|
|
|
|
fn post_index() -> &'static str {
|
|
|
|
"success"
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/token")]
|
2018-09-08 13:03:41 +00:00
|
|
|
fn token(t: CsrfToken) -> String {
|
2018-09-08 13:01:39 +00:00
|
|
|
::std::str::from_utf8(t.value()).unwrap().to_owned()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/csrf")]
|
|
|
|
fn csrf() -> &'static str {
|
|
|
|
"violation"
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/ex1")]
|
|
|
|
fn get_ex1() -> &'static str {
|
|
|
|
"get-ex1"
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/ex1")]
|
|
|
|
fn post_ex1() -> &'static str {
|
|
|
|
"post-ex1"
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/ex1-target")]
|
|
|
|
fn target_ex1() -> &'static str {
|
|
|
|
"target-ex1"
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/ex2/<_dyn>")]
|
|
|
|
fn post_ex2(_dyn: String) -> &'static str {
|
|
|
|
"valid-dyn-req"
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/ex2-target/<dyn>")]
|
|
|
|
fn target_ex2(dyn: String) -> String {
|
|
|
|
dyn
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/static/something")]
|
|
|
|
fn static_route() -> ::rocket::response::content::Content<&'static str> {
|
2018-09-08 13:03:41 +00:00
|
|
|
::rocket::response::content::Content(
|
|
|
|
::rocket::http::ContentType::HTML,
|
|
|
|
"<div><form></form></div>",
|
|
|
|
)
|
2018-09-08 13:01:39 +00:00
|
|
|
}
|
|
|
|
}
|