Friday, January 27, 2017

Rocket Rocks! Using FromFormValue Traits to protect your website

TLDR; Let the sourcecode speak for itself, here it is: https://github.com/stephanbuys/rocket-wasp

When you develop a public facing website that accepts user input you have to assure that your input is valid, there are a myriad of risks that face you, but don't take my word for it, see https://www.owasp.org.

Introducing OWASP (weird application super protected)

Recently a shiny new web framework for Rust was announced called rocket (http://rocket.rs), and for my own part rocket just clicked with me. What's truly great about rocket is it's pervasive use of the Rust type system and Traits, allowing you to write inspired apps with minimal code, that just "makes sense" and is intuitively readable and understandable.

In order to support client requests we define routes in rocket.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[get("/")]
fn index() -> &'static str {
    "Nothing here."
}

#[get("/process?<query_string>")]
fn insert(query_string: QueryString) -> String {
    format!("Safe to process: {:#?}", query_string)
}

fn main() {
    rocket::ignite().mount("/", routes![index, insert]).launch();
}

The code above defines two routes and mounts them in main. The first route is just a catch-all for browsers, the second route, process accepts a query string of type QueryString which is defined as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#[derive(Debug)]
struct QWordString(String);
#[derive(Debug)]
struct QSmallNumber(u32);
#[derive(Debug)]
struct QBoolean(bool);

#[derive(Debug, FromForm)]
struct QueryString {
    w: QWordString,
    s: QSmallNumber,
    b: QBoolean
}

As can be noted we define types for each query parameter that we're expecting. With the struct defined above we expect a request a with three parameters w, s and b, for example:

curl 'http://localhost:8000/process?w=foo&s=10&b=true'

As can be seen in the first listing (the process route, line 8), will return a String when called, which it returns to the user with the output below, the app claims that this output is "Safe to process", well that's great, but how can we know that.

Safe to process: QueryString {
    w: QWordString(
        "foo"
    ),
    s: QSmallNumber(
        10
    ),
    b: QBoolean(
        true
    )
}

This is where rocket and its built-in traits come in, in this case we're interested in the trait FromFormValue. Rocket takes care of parsing the query string into a &str, we then need to implement the trait for our types. The rocket documentation illustrates it's use here: https://api.rocket.rs/rocket/request/trait.FromFormValue.html.

QWordString only accepts the words "foo" and "boo", QSmallNumber only allows unsigned integers under 100 and QBoolean will accept "yes", "no", "true", "false", "1" and "0".

Lets test it as follows:

curl 'http://localhost:8000/process?w=foo&s=101&b=true'


            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8">
                <title>404 Not Found</title>
            </head>
            <body align="center">
                <div align="center">
                    <h1>404: Not Found</h1>
                    <p>The requested resource could not be found.</p>
                    <hr />
                    <small>Rocket</small>
                </div>
            </body>
            </html>

As can be seen, our number parameter, s, was not accepted (its above 100).

In order to accomplish the input validation we implement the FromFormValue trait for our types.

QWordString:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
impl<'v> FromFormValue<'v> for QWordString {
    type Error = &'v str;

    fn from_form_value(form_value: &'v str) -> Result<QWordString, &'v str> {
        //Is the string one of our approved ones? (foo or boo)
        let re = Regex::new(r"^(foo|boo)$").unwrap();
        if re.is_match(form_value) {
            return Ok(QWordString(String::from(form_value)));
        }
        Err("unacceptable word parameter")
    }
}

Here we use the regex crate and a regex that only allows the words "foo" or "boo". It will throw a "unacceptable word parameter" error if the string doesn't strictly match.

QSmallNumber:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
impl<'v> FromFormValue<'v> for QSmallNumber {
    type Error = &'v str;

    fn from_form_value(form_value: &'v str) -> Result<QSmallNumber, &'v str> {
        //Is the number small? Under 100?
        let n = match form_value.parse::<u32>() {
            Ok(n) => n,
            Err(_) => return Err("not a number, or can't convert to u32")
        };
        if n < 100 {
            return Ok(QSmallNumber(n));
        }
        Err("unacceptable number parameter")
    }
}

In this snippet we only accept valid number (line 7), and only if it is below 100 (line 11).

And lastly.
QBoolean:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
impl<'v> FromFormValue<'v> for QBoolean {
    type Error = &'v str;

    fn from_form_value(form_value: &'v str) -> Result<QBoolean, &'v str> {
        //True values
        let true_re = Regex::new(r"^(yes|true|1)$").unwrap();
        if true_re.is_match(form_value) {
            return Ok(QBoolean(true));
        }
        //False values
        let false_re = Regex::new(r"^(no|false|0)$").unwrap();
        if false_re.is_match(form_value) {
            return Ok(QBoolean(false));
        }
        Err("unacceptable bool parameter")
    }
}

Where we again use the regex crate to check for the acceptable true and false values.

As can be seen, this mechanism can be incredibly powerful and can be used to assure that only valid input gets processed on the server. Have a look at the final product at https://github.com/stephanbuys/rocket-wasp, the code doesn't have to be super verbose and a basic understanding of rust types and traits is really the only bits that the developer needs to understand, rocket just gets out of the way.