rwf2 / Rocket

A web framework for Rust.
https://rocket.rs
Other
23.52k stars 1.52k forks source link

Add (more) CSRF protection #14

Open SergioBenitez opened 7 years ago

SergioBenitez commented 7 years ago

CSRF is a thing. Protect against it automatically in forms.

jrozner commented 7 years ago

Tying this specifically to forms is a bad idea because it makes supporting Single Page Applications (SPAs) written in JavaScript more difficult and can break support for AJAX requests that use json/xml APIs. It's common for those applications to send the token in an HTTP request header instead of in the form body, especially if the body isn't a standard form encoded value (eg. JSON, XML), because it's easier to simply add a header before a request is sent than have to rewrite the DOM each time a request is made.

A common way to handle this is to provide two code paths for the check determined by the presence and value of the X-Requested-With header. jQuery specifically, and other AJAX libraries, set the X-Requested-With header before sending a request and set the value to "XMLHttpRequest". In the case that the header isn't set or it's not that value defaulting to looking in the form payload for the specified value is the correct place to find the token. Otherwise, if the header is set and it denotes an xhr you look in the header for the token.

It's also worth noting that a reasonable implementation should depend on sessions existing for storage of the token. I'm happy to help work on this if it's not already being done.

golddranks commented 7 years ago

I'd like to also have CSRF protection by default on when accessing the site with any mutating HTTP method, like POST, PUT or DELETE. At least check the origin + referer by default in these cases, and make it easy to additionally annotate routes that need checking, plus make it easy to have a HTTP header or cookie based check too.

scull7 commented 7 years ago

Is there anyone working on this? If not, I wouldn't mind having a go at it. I believe that checking for a header token and then falling back to the form body would cover most cases (80/20 rule).

SergioBenitez commented 6 years ago

With a rather large sigh of grief, I'm pushing this to 0.4.

Implementing this in a simple, efficient, and straightforward manner has been a challenging endeavor. While it's simple to validate the incoming request against CSRF, it's not simple to emit a response with the appropriate information for later validation. In particular, we'd like to make it easy and efficient to insert a CSRF token into a web form in a template. To complete the defense mechanism, we need to insert the same token into a cookie. This presents a challenge: how do we know that a token has been inserted into a template so that we can then use the same token as a cookie in the response?

I have two candidate solutions, each with a somewhat large drawback.

  1. Use a global per-thread token generator.

    The per-thread token generator would expose three functions/methods, get_token() -> Token, reset() and last_token() -> Option<Token>. When a new request is received, reset() is called. The call should be a noop under most conditions. A template helper is exposed that calls get_token() to get an appropriate CSRF token. When Template generates the final response, it calls last_token() after rendering the template to determine if a CSRF token was used. If one was used, it is inserted into the response as a cookie.

    There are two major drawback here. First, we've introduced "global" state. Rocket works hard to keep all state local; this would be the only "global" state. Second, this approach won't work when more than one task maps to a single thread (i.e, with coroutines or futures).

    1. Add support for rendering state to existing template engines.

      In this approach, each template engine is extended with the ability to hold "rendering" state: state that is generated by helpers at render time. This means that we can implement a CSRF token generator helper than records, in local template state at render time, if and which token was generated. This even allows multiple tokens to be generated for a given template. The Template responder then simply queries this state after rendering the template to retrieve the CSRF token, if any, and inject it as a cookie into the response.

      The drawback to this approach is that we need buy-in from template engine authors. It also means that we would depend on this feature being present for any newly supported template engines if automatic CSRF protection is desired.

I believe that choice #2 above is a better approach. I'm against adding any kind of global state, though I'll note that this is precisely what other frameworks use to implement this kind of automatic CSRF protection. Any thoughts, suggestions, or ideas on this matter would be greatly appreciated. This is an important feature to have, and I'm excited to really nail down a good solution here.

jrozner commented 6 years ago

I'm not following why global state is needed for implementing the token system or why the design of 1 is being considered. Validation and generation can always happen at request time before a response with the template ever occurs. Typically the way most frameworks handle this is when a request comes in the framework determines if the request requires a token and if it needs to be validated. If not, it's simply a nop and the the request/response cycle goes through as expected. In the case where a token needs to be validated one of two events can happen:

  1. The token is valid and a new token is generated/rotated (this should be stored in the session) and the request continues
  2. The token is invalid/missing and a new token is generated/rotated (this should be stored in the session) and the request is stopped

You should never need to know a prior token's value and that information should not need to be kept around or generated at template generation time. Furthermore you also shouldn't need to expose a global, the template's scope just needs to include access to the session where the token can be taken out of it.

SergioBenitez commented 6 years ago

As I said in my previous comment:

While it's simple to validate the incoming request against CSRF, it's not simple to emit a response with the appropriate information for later validation.

Validation is easy, as you've determined. As is generating a new token. The hard part (for us) is inserting that token in two places: a template and a cookie. We want to use a new token for each response requiring one, and we don't want to generate a new token unless we're going to eventually use it. Thus, generating a new token each time a request is received is a non-starter.

You say:

...the template's scope just needs to include access to the session where the token can be taken out of it.

But that's exactly part of the issue; how do you do that? You've already assumed that there's some global session that we can simply refer to: there isn't. We also can't simply inject information into a template's scope: the user specifies the scope. It is conceivable that we can extend existing template engines with a second, framework provided scope. This would be equivalent to my second proposal except with information flowing the inverse direction.

Let me try to be as clear as possible. In a template, a user should be able to write something like:

<form ..>
  <!-- in practice, a form helper will generate the form tag AND the token field -->
  {% csrf_token %}
</form>

Rocket should generate the CSRF token hidden field from the csrf_token helper. Then, when a response is sent to the client, Rocket should include a CSRF token cookie with the same value as that in the form field. Rocket shouldn't send a cookie unless a token was used (though there's no harm in doing so), and Rocket should generate a new token for each response requiring a token.

You should never need to know a prior token's value and that information should not need to be kept around or generated at template generation time.

In my proposal, last_token() returns the token that was just generated so that a cookie can be created with its value, not a token used in a previous response. This is required information; there's no way around it. Something needs to know which token value was used in the response's HTML to include it in the response as a cookie. Other web frameworks use global state (such as server-side sessions) to do this; we don't have that ability.

jrozner commented 6 years ago

I understand the issue now. One suggestion I have, and this may not be the popular opinion or desired behavior, is to not try and push the token injection into the templates as part of rocket. I think unless rocket is dictating the template/rendering engine. The reason I say this is a couple reasons: First, There's going to be a lot of variation and support needed as well as buy in from all the developers working on the engines. Second, The updating of the tokens itself is going to be very app specific and going to be difficult to impossible to handle without providing a frontend js component like Rails does with it's UJS. If the application is using xhr the template may not be re-rendered and rotating the token on the server side (especially if it wasn't validated) will immediately break the page.

In my day job we had to solve this problem for apps that use our security plugin and what we did was append some JavaScript to the end of the any responses that have a handful of characteristics (content-type, etc.). This puts a small wrapper around the xhr code as well as adds an event listener into the dom and delegates to a handful of elements (submit, a, button) to add tokens into xhr requests from a cookie do some clever dom manipulation to drop the token into forms before the request is submitted. This let's us avoid parsing the response and manipulating it to try and put the tokens into the forms. The parsing wont be an issue for Rocket because the location is going to explicitly be set for where insertion is needed but this would solve the issue of tokens updating and the dom not being rewritten.

This solution is sort of out of the scope of Rocket as a rust based web framework but it's a solution that we've found solves some of these problems well.

DemiMarie commented 6 years ago

@SergioBenitez May I ask why?

Generating a token is easy. It should be as simple as grabbing a few bytes from an optimized implementation of ChaCha20 or (if AES-NI is available) AES256-CTR. Much, much less effort than the upstream proxy server spent decrypting the incoming HTTPS request.

Regarding sessions: Rocket already supports authenticated encryption of cookies. That means that we can store the token client side without any security loss. Client side code can still read the token, but can’t tamper with it without being detected (resulting in a 400 Bad Request error), nor can it make any sense of its contents.

Finally, just checking Origin and Referer headers would go a LONG way towards improving security.

jrozner commented 6 years ago

@DemiMarie so I'm sure that Sergio will have more to add to this but it's not so much the generation that's difficult but the submitting the tokens with requests. The problem becomes getting the token into the DOM for form based requests and into XHR requests. If the goal is to provide server side templating with support for csrf tokens (which it appears this is a goal) the hard part is getting the token into the template and updating the template.

Rails solved this by providing some helpers with global state on the server side rendering end and some additional JS that was part of their UJS package. Global state is undesired in this case which complicates the problem significantly.

One solution that might be worth looking at is providing two separate pieces of middleware. One to handle generation/validation of tokens as well as adding them into responses for JS SPA frameworks, such as angular, to use that already have built in support for this. Then provide a second middleware or component that adds support for the helpers on the server side rendering.

If you're looking for more ideas I spoke at DEF CON this year about a solution I outlined above that included some JS that is injected to provide support client side. It seemed that there wasn't interest in a solution like this but just to offer more options. https://www.youtube.com/watch?v=foFf4gMbL2w

DemiMarie commented 6 years ago

@jrozner What about having some sort of per-request state?

I really really really don’t want the solution to be dependent on JS. I personally use NoScript, for instance, and believe that a simple form should not require JS to submit. There are also accessibility issues, if I recall correctly, though I don’t remember what they are.

jrozner commented 6 years ago

@DemiMarie The issue isn't so much about the state at the handler point but providing the state to the templating engine in a way that the templating engine can actually use it. The state in sessions is probably fine at the handler level but getting that into a template is more complicated.

Part of the problem is attempting to solve this for all cases and Rocket not being opinionated allowing you to bring basically any templating with you or bring none (eg. no server side rendering and returning JSON).

I realistically don't see a possible solution that isn't split into multiple parts because a non-js dependent solution that allows for server side rendering but also for SPAs (or at least requests that don't re-render) is incompatible and solve very different problems. One problem is generation, validation, and storage of tokens while the other is insertion of tokens into requests and updating tokens after responses.

Supporting arbitrary templating engines isn't really a csrf problem but is required to be solved for server side rendering in Rocket and providing necessary support. Either adapters likely will need to be provided for specific templating engines to provide a common interface for this or maybe an alternative solutions. What I'm proposing here is build the functionality for dealing with generation, validation, and providing tokens in responses so this becomes available now for a large majority of users and applications that don't have the specific requirements you do. At this point, whether you like it or not, NoScript is not viable for most parts of the web and in most cases JS based SPAs have won and their frameworks handle the token insertion for you.

ssokolow commented 6 years ago

At this point, whether you like it or not, NoScript is not viable for most parts of the web and in most cases JS based SPAs have won and their frameworks handle the token insertion for you.

Unless you've got some objective research here, I'm going to say we're both suffering from selection bias. I certainly don't see them as having won, nor do I see NoScript as being non-viable.

jrozner commented 6 years ago

@ssokolow I'll cede to that and accept that you might be right here. Given trends in web development I doubt it but it's very possible. However, the earlier point about separate problems I think still holds and is relevant.

seanlinsley commented 6 years ago

https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Checking_the_Referer_Header

Checking the Referer is a commonly used method of preventing CSRF on embedded network devices because it does not require any per-user state. This makes Referer a useful method of CSRF prevention when memory is scarce or server-side state doesn't exist. This method of CSRF mitigation is also commonly used with unauthenticated requests, such as requests made prior to establishing a session state which is required to keep track of a synchronization token.

It appears that checking Origin & Referer are sufficient, except when:

ssokolow commented 6 years ago

It appears that checking Origin & Referer are sufficient, except when:

Here are another two reasons which CSRF protection via the Referer header is inadequate:

  1. The Referer header has never been more unreliable, thanks to the concerns about being tracked around the web and extensions which meddle with the header to prevent the risk that sites like Javascript CDNs and Google Font Library might be using IP+Referer to track them.

  2. Proper CSRF protection is robust against attacks using ALL kinds of user agents, including ones that can fully customize the headers that get sent. That's why the OWASP page you link puts the word "AND" in all caps when saying that you should check Origin/Referer AND a CSRF token.

jrozner commented 6 years ago

@seanlinsley suggesting that it's reasonable to simply have an application not work for users behind a proxy is a pretty concerning stance. Especially if the application can't explain why it's not working for the user. We shouldn't make users choose between security and functionality when there are solutions available that don't require that.

seanlinsley commented 6 years ago

@ssokolow could you link to such browser extensions? Is there data to suggest that they block the referer header indiscriminately on any website, even when navigating from page to page on the same domain? (as opposed to stripping the header once it becomes a cross-domain request)

I'm not sure I understand the argument in point 2. Could you elaborate?

@jrozner AFAICT proxies in general don't remove the header unless specifically configured to.

This is entirely explainable to end users. For example:

Unable to verify your request. Please try again after turning off privacy extensions in your browser, or moving to a different internet connection.

ssokolow commented 6 years ago

Firefox has it built in through these about:config keys:

...though I prefer to use an extension like RefControl (legacy) or Smart Referer (WebExt) which implements a whitelist similar to NoScript.

(RefControl, which I've used for years and which I'll continue to use until I get off Firefox 52ESR, allows you to choose between Block (no referrer), Forge, and Allow, both for the default policy and for per-domain rules... including ones which only apply to 3rd-party requests.)

For Chrome, you either launch it with --no-referrers (also possible by editing Preferences) or use an extension like Referer Control which, again, provides NoScript-like support for per-site rules.

DemiMarie commented 6 years ago

That still means that some users behind corporate proxies cannot use the app. This is bad!

On Nov 25, 2017 8:06 PM, "Sean Linsley" notifications@github.com wrote:

@ssokolow https://github.com/ssokolow could you link to such browser extensions? Is there data to suggest that they block the referer header indiscriminately on any website, even when navigating from page to page on the same domain? (as opposed to stripping the header once it becomes a cross-domain request)

I'm not sure I understand the argument in point 2. Could you elaborate?

@jrozner https://github.com/jrozner AFAICT proxies in general don't remove the header unless specifically configured to.

This is entirely explainable to end users. For example:

Unable to verify your request. Please try again after turning off privacy extensions in your browser, or moving to a different internet connection.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/SergioBenitez/Rocket/issues/14#issuecomment-346976753, or mute the thread https://github.com/notifications/unsubscribe-auth/AGGWBzBez85iSeV0cCq88lBLL-AcQHrQks5s6LmqgaJpZM4KF2dV .

ssokolow commented 6 years ago

...as for point 2, my argument is that I don't think it's robust enough to rely exclusively on headers for CSRF protection and the OWASP document seems to agree.

The whole point of the token-based approach is twofold:

  1. It's robust in the face of overly-strict security measures. (No corporate proxy would block HTTP POST or session cookies because they're so fundamentally necessary.)
  2. No matter what a user's browser may allow or do to the request, it's still necessary that two requests be made, in the proper order, within the timeout period for the CSRF token, and that the token be communicated from one request to the other in a sufficiently automatic fashion... and, if at all possible, without the user noticing and calling their bank to report the fraudulent transaction. That's a much trickier thing to find an exploit to allow.

Heck, I watched a talk where one of the takeaways was "Don't assume your design's custom fonts will load. There are still corporate proxies that don't have headers like Origin on their whitelist."

seanlinsley commented 6 years ago

FWIW the above-linked Smart Referer browser extension only removes the header when navigating cross-domain.

From my point of view, if you have corporate customers you can either afford the budget to write a ton of custom code, or you can get them to whitelist your site so the headers aren't stripped.

Given @SergioBenitez's desire not to add global state, and the complexity involved in building a common API for multiple possible template engines, this seems like a reasonable solution that will work for almost all users. The perfect is the enemy of the good, etc.

DemiMarie commented 6 years ago

I think that global (well, request-local) state is not a big deal.

On Nov 26, 2017 2:09 PM, "Sean Linsley" notifications@github.com wrote:

FWIW the above-linked Smart Referer browser extension only removes the header when navigating cross-domain.

From my point of view, if you have corporate customers you can either afford the budget to write a ton of custom code, or you can get them to whitelist your site so the headers aren't stripped.

Given @SergioBenitez https://github.com/sergiobenitez's desire not to add global state, and the complexity involved in building a common API for multiple possible template engines, this seems like a reasonable solution that will work for almost all users. The perfect is the enemy of the good, etc.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/SergioBenitez/Rocket/issues/14#issuecomment-347030791, or mute the thread https://github.com/notifications/unsubscribe-auth/AGGWB5HbNAnGBcvnsl2PZa7UMxDGVFCOks5s6bdogaJpZM4KF2dV .

avanov commented 6 years ago

From my point of view, if you have corporate customers you can either afford the budget to write a ton of custom code, or you can get them to whitelist your site so the headers aren't stripped.

This is not how B2B works, though, and you rarely have a luxury of an extra budget or access to established corporate security policies. And the customers in this setting really care about security and ability to solve as many domain problems (and their specific cases) with the tool of choice as possible, and they rarely care about specifics the tool is implemented with, i.e. they don't care if CSRF is managed by a global state or some other magical approach that doesn't require the state. Hence, the criteria list is:

I agree that the perfect is the enemy of the good, yet I think this GH issue is the perfect counter-example of this motto - it's been more than a year since the initial proposition, and there's no production-ready working solution to CSRF handling in Rocket to the moment, even a mediocre one.

DemiMarie commented 6 years ago

BINGO!!!!!!!!

On Nov 26, 2017 2:41 PM, "Maxim Avanov" notifications@github.com wrote:

From my point of view, if you have corporate customers you can either afford the budget to write a ton of custom code, or you can get them to whitelist your site so the headers aren't stripped.

This is not how B2B works, though, and you rarely have a luxury of an extra budget or access to established corporate security policies. And the customers in this setting really care about security and ability to solve as many domain problems (and their specific cases) with the tool of choice as possible, and they rarely care about specifics the tool is implemented with, i.e. they don't care if CSRF is managed by a global state or some other magical approach that doesn't require the state. Hence, the criteria list is:

  • does it cover our use cases?
  • is it reliable?
  • is it secure?
  • how much it cost us on our use cases?

I agree that the perfect is the enemy of the good, yet I think this GH issue is the perfect counter-example of this motto - it's been more than a year since the initial proposition, and there's no production-ready working solution to CSRF handling in the Rocket to the moment, even a mediocre one.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/SergioBenitez/Rocket/issues/14#issuecomment-347032831, or mute the thread https://github.com/notifications/unsubscribe-auth/AGGWB9CwIwSHjlg3rBWm1yYirisFyvFtks5s6b7fgaJpZM4KF2dV .

SergioBenitez commented 6 years ago

This topic has garnered a very healthy amount of discussion, and for good reason, too! CSRF is an important attack vector to protect against, not least because it is so commonly seen in the wild. I want to reiterate that I'm committed to ensuring that Rocket protects against CSRF in version 0.4.

For the reasons I discussed in my previous comments (https://github.com/SergioBenitez/Rocket/issues/14#issuecomment-305882710, https://github.com/SergioBenitez/Rocket/issues/14#issuecomment-305955256), there are unique challenges to implementing automatic CSRF protection in Rocket. The root cause of many of these challenges is the desire to avoid global state. This desire isn't simply an argument for elegance; any notion of global state jeopardizes the ability to run multiple instances of one Rocket application in the same process. This exact scenario occurs when running tests, for instance.

I've been working on a solution that resolves these issues. The solution closely aligns with option 2 in my original comment ("Add support for rendering state to existing template engines.") but takes a slightly different approach than what was originally proposed. While I'm not ready to announce the exact mechanisms yet, the solution will resolve these issues. Additionally, this approach will also protect Rocket applications against XSS attacks. In fact, if all goes well, Rocket will provide a compile-time guarantee against CSRF and XSS attacks, something I'm extremely excited about!

I'm looking forward to announcing more about these efforts as progress continues. Until then, I appreciate all of the commentary. Your support, in any form, is invaluable.

DemiMarie commented 6 years ago

@SergioBenitez

Additionally, this approach will also protect Rocket applications against XSS attacks. In fact, if all goes well, Rocket will provide a compile-time guarantee against CSRF and XSS attacks, something I'm extremely excited about!

That is FANTASTIC!!!!!!

Does that also include a guarantee that the application doesn’t (say) try to interpolate user input into a <style> or <script> tag or an <onclick> attribute? Obviously a full guarantee requires control over client-side JS (the code could include something like eval(document.getElementById('container').innerHTML)).

jrozner commented 6 years ago

Will the solution you're working on support applications that don't use server side rendering? Most frontend frameworks that utilize xhr depend on the token being inserted into an HTTP response headers instead of in a rendered template?

japrogramer commented 6 years ago

In django we check the header and a http only header, I think that will take care of that concern.

novacrazy commented 6 years ago

How is this coming along?

seanlinsley commented 6 years ago

I'd be happy to help with this @SergioBenitez

mb1986 commented 6 years ago

Here is my current approach to CSRF protection:

use rocket::{Rocket, Request, Response, Data};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{StatusClass, Cookie, Method};
use rocket::response::Failure;
use rocket::http::Status;
use uuid::Uuid;

const XSRF_TOKEN: &str = "XSRF-TOKEN";
const X_XSRF_TOKEN: &str = "X-XSRF-TOKEN";

#[get("/403")]
fn error_403() -> Failure {
    Failure(Status::Forbidden)
}

pub struct CsrfProtection;

impl Fairing for CsrfProtection {

    fn info(&self) -> Info {
        Info {
            name: "CSRF Protection",
            kind: Kind::Request | Kind::Response | Kind::Attach
        }
    }

    fn on_request(&self, request: &mut Request, _: &Data) {
        match request.method() {
            Method::Post | Method::Put | Method::Patch | Method::Delete => {
                if !request.cookies()
                    .get(XSRF_TOKEN)
                    .map(Cookie::value)
                    .and_then(|cookie_token|
                        request.headers()
                            .get_one(X_XSRF_TOKEN)
                            .map(|header_token| cookie_token == header_token)
                    ).unwrap_or(false) {
                        let new_uri = format!("/403#{}", &request.uri());
                        request.set_uri(new_uri);
                        request.set_method(Method::Get);
                    }
            },
            _ => (),
        }
    }

    fn on_response(&self, request: &Request, response: &mut Response) {
        if response.status().class() == StatusClass::Success {
            response.set_header(
                &Cookie::build(XSRF_TOKEN, Uuid::new_v4().hyphenated().to_string())
                    .path("/")
                    .finish()
            );
        }
    }

    fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
        Ok(rocket.mount("/", routes![error_403]))
    }
}

pub fn path<'r>(req: &'r Request) -> &'r str {
    req.uri().fragment()
        .unwrap_or(req.uri().path())
}

The use of the above Fairing:

rocket::ignite()
    .attach(security::csrf::CsrfProtection)
    // ...
    .launch();

And path resolution in catcher:

#[error(403)]
fn forbidden(req: &Request) -> Json {
    Json(json!({
        "status": 403,
        "message": "Forbidden",
        "path": path(req)
    }))
}
ZoeyR commented 5 years ago

Its a quite recent development but the SameSite attribute on cookies may also be a way to prevent these sorts of attacks. Although it probably woudn't be a good default for Rocket since its such a new standard and as such isn't widely supported.

SergioBenitez commented 5 years ago

@dgriffen Rocket has been using SameSite by default for a while, so Rocket applications are already protected against CSRF (for private cookies, which all sensitive material should be stored in) on modern browsers. It's a nice mechanism that works for many cases, but it is not robust enough (in the same way only checking Origin/Referer isn't) for the real world. As such, this issue stands.

ZoeyR commented 5 years ago

@SergioBenitez can you elaborate on the cases where it isn't robust enough? Is it just because some browsers may not properly implement it (similar to how some browsers may not send Origin/Referer) or is there some other issue even when browsers do properly implement it?

SergioBenitez commented 5 years ago

@dgriffen The SameSite attribute is still a draft and isn't supported by some major browsers including the current major releases of Safari on macOS and iOS (though is in the next major release). According to caniuse.com, you can expect about 2/3 of your users' browsers to support the attribute, leaving 1/3 of users in the dust.

ssokolow commented 5 years ago

Also, a CSRF token can be used to ensure users are following a certain request flow, while something like SameSite would allow an attacker to attack any part of your site from any other part of your site if they find an XSS vulnerability.

SergioBenitez commented 5 years ago

I'm pushing this to 0.5 in an effort to get 0.4 out as soon as possible. Ideally this is in a 0.4 point release, however.

PrismaPhonic commented 5 years ago

Any update to this feature?

Jayshua commented 4 years ago

So I thought I'd just put in my two cents. @jrozner's comment suggest an answer that makes sense to me, but maybe I'm just missing something.

Part of the problem is attempting to solve this for all cases and Rocket not being opinionated allowing you to bring basically any templating with you or bring none.

Since rocket supports simply not using templates, the CSRF support should just be totally independent of templates. Therefore, whatever method you use to create a response, it's up to you to insert the CSRF token. Something like this maybe:

#[get("/login")]
fn login(form_data: FormData, csrf: Csrf) -> Template {
   ...
   Template::render("login", json!({
    csrf: csrf.token(),
    ...
  })
}

Or, if you're not using templates, provide the token to whatever system you are using to generate a response.

This seems like a reasonable solution to me because, ultimately, it is up to you to get the CSRF into the DOM. Even if the templating system is extended to support framework state as was suggested, it would still be up to the user to get the token into the DOM anyway with {% csrf %}.

But again, maybe I'm just missing something here.

kylone commented 4 years ago

@dgriffen The SameSite attribute is still a draft and isn't supported by some major browsers including the current major releases of Safari on macOS and iOS (though is in the next major release). According to caniuse.com, you can expect about 2/3 of your users' browsers to support the attribute, leaving 1/3 of users in the dust.

Support for SameSite is now more wide spread, if you're only considering supported browsers, according to caniuse.com.

Aloso commented 3 years ago

The way I see it, if you don't want to rely on browser support for SameSite cookies, sessions are required; it's the only way to make sure that two different users don't have the same CSRF token.

If you have a stateless API without a session cookie (e.g. a REST API), you need another way to authenticate each request, which makes CSRF basically impossible.

You can implement a Session type containing a CSRF token as a request guard. You need a global, concurrent hashmap containing the sessions, so parts of it must be implemented in user code.

Proof of concept:

#[derive(PartialEq)]
struct CsrfToken(String);

#[derive(Hash)]
struct SessionId(String);

struct Session {
    csrf_token: CsrfToken,
    // ...other fields
}

impl<'a, 'r> FromRequest<'a, 'r> for &'r mut Session { ... }

impl<'a, 'r> FromRequest<'a, 'r> for CsrfToken {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<CsrfToken, ()> {
        let token = CsrfToken(request.get_param("CSRF_TOKEN")?.to_string());
        let session = request.guard::<&mut Session>()?;
        if &session.csrf == &token {
            Outcome::Success(token)
        } else {
            Outcome::Failure(())
        }
    }
}

#[derive(Default)]
struct SessionMap {
    sessions: DashMap<SessionId, Session>,
}

rocket::ignite()
    .manage(SessionMap::default())
DemiMarie commented 3 years ago

What about the stateless approach based on Origin and Referer headers?

Aloso commented 3 years ago

@DemiMarie according to this article a server shouldn't rely on the fact that either of these headers is present. However, the Origin and Referer headers can be verified, if they are present, in addition to using a cryptographically strong CSRF token.

I just learned about another nice way to store CSRF tokens without requiring a session: The double submit cookie technique. It means that the CSRF token is stored in an HTTP cookie and not on the server, which eliminates the need for sessions or global state.

For additional security, the cookie containing the CSRF token can be encrypted or hashed with a secret salt.

kotovalexarian commented 3 years ago

I've started working on some CSRF protection: https://github.com/kotovalexarian/rocket_csrf

It is not safe enough maybe because it doesn't encrypt authenticity token included in forms. I'm not sure why it's needed to encrypt it but Ruby on Rails does this. Also my implementation doesn't provide any template helpers. However it may be a good working prototype to see how CSRF protection can be done in Rocket in idiomatic way. Tell me if it is non-idiomatic.

I'm ready to transfer crate name (rocket_csrf) to @SergioBenitez if needed.

Here is an example:

#![feature(decl_macro)]

#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;

use rocket::response::{Flash, Redirect};
use rocket::request::{FlashMessage, Form};
use rocket_contrib::templates::Template;
use rocket_csrf::CsrfToken;

#[derive(Serialize)]
struct TemplateContext {
    authenticity_token: String,
    flash: Option<String>,
}

#[derive(FromForm)]
struct Comment {
    authenticity_token: String,
    text: String,
}

fn main() {
    rocket::ignite()
        .attach(rocket_csrf::Fairing::new())
        .attach(Template::fairing())
        .mount("/", routes![new, create])
        .launch();
}

#[get("/comments/new")]
fn new(csrf_token: CsrfToken, flash: Option<FlashMessage>) -> Template {
    let template_context = TemplateContext {
        authenticity_token: csrf_token.0,
        flash: flash.map(|msg| format!("{}! {}", msg.name(), msg.msg())),
    };

    Template::render("comments/new", &template_context)
}

#[post("/comments", data = "<form>")]
fn create(csrf_token: CsrfToken, form: Form<Comment>) -> Flash<Redirect> {
    if let Err(_) = csrf_token.verify(&form.authenticity_token) {
        return Flash::error(
            Redirect::to(uri!(new)),
            "invalid authenticity token",
        );
    }

    Flash::success(
        Redirect::to(uri!(new)),
        format!("created comment: {:#?}", form.text),
    )
}
JonasCir commented 3 years ago

Thank you for working on this! :) One improvement could be to create an attribute-like macro which takes care of the token verification and the early return such that it gets harder to introduce errors. Or maybe post and put macros could take an additional argument e.g., #[post("/comments", data = "<form>", csrf)] or similar.

JonasCir commented 3 years ago

I am not sure about the token encryption, but I'll do some research and get back to you

kotovalexarian commented 3 years ago

One improvement could be to create an attribute-like macro which takes care of the token verification and the early return such that it gets harder to introduce errors.

This is mentioned in TODO. It requires data guard because usual guards can't extract form data from request AFAIK. Anyway, thank you for mentioning this, I'll try to focus on this task first.

NilsIrl commented 3 years ago

It requires data guard because usual guards can't extract form data from request AFAIK.

I think this may still require #775

kotovalexarian commented 3 years ago

Yes. I just found that only single data guard can be used in a route. So there is no way to avoid authenticity token validation code in a route for now.

mjanda commented 3 years ago

@kotovalexarian it was possible to peek into first bytes of request data and read token value this way, but it's impossible in current master. See https://github.com/SergioBenitez/Rocket/issues/1438