Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSRF Support #606

Merged
merged 9 commits into from Oct 24, 2014
Merged

CSRF Support #606

merged 9 commits into from Oct 24, 2014

Conversation

iamjem
Copy link
Contributor

@iamjem iamjem commented May 11, 2014

This is a first attempt at adding CSRF support. I've modeled the implementation closely after the Express.js CSRF middleware and Django's CSRF middleware.

I preferred some of the subtleties found in Express' version, namely that tokens weren't stored "as-is" in the session cookie, but rather a secret that was used to generate them lazily. Django had some additional strictness with the HTTP referer which I thought was important as well.

Usage is pretty simple. Add csrf.CsrfFilter to the app's filters (it must come after the revel.SessionFilter), and you're good to go. To add CSRF fields to a form, use the template tag {{ csrftoken . }}. The filter adds a function closure to the RenderArgs that can pull out the secret and make the token as-needed, caching the value in the request. Ajax support provided through the X-CSRFToken header.

The csrf.RefreshSecret should be called after a user logs in.

Like I said, first stab at things. Happy to make/discuss improvements.

@brendensoares brendensoares added this to the v0.11 milestone May 17, 2014
func init() {
revel.TemplateFuncs["csrftoken"] = func(renderArgs map[string]interface{}) template.HTML {
tokenFunc, ok := renderArgs["_csrftoken"]
if !ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 2 lines instead of 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, definitely some rough edges left from the refactoring that needs to be cleaned up still. When we've got some consolidated feedback I'll go through and incorporate everything at once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again. Is your comment about line 134? The tokenFunc variable gets used outside of the if statement, which is why I didn't declare them in the if's initialization statement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do something like

    if tokenFunc, ok := renderArgs["_csrftoken"];!ok {
        panic("REVEL CSRF: _csrftoken missing from RenderArgs.")
    } else {
        return template.HTML(tokenFunc.(func() string)())
    }

@brendensoares
Copy link
Member

This is a great first stab :) @revel/core any input?

Thanks for the tests/examples, too. That makes things a lot easier! As soon as we push out v0.10 we cna look into this more closely.

@landaire
Copy link
Contributor

Mad props! Great implementation and pull request overall. I'm just curious though, why go with a UUID instead of using a SHA hash or something among those lines?

@pushrax
Copy link
Contributor

pushrax commented May 17, 2014

Probably based in on the behaviour that was corrected in #552. As @landr0id mentioned, definitely switch this one over as well.

@iamjem
Copy link
Contributor Author

iamjem commented May 17, 2014

So instead of using simpleuuid for the random strings (which is using time.Now), instead use crypto/rand as seen in session.go in the dev branch?

From what I've seen, this is more common than using UUID generators. I used the UUID simply because thats what was in use at the time for sessions in master.

@pushrax
Copy link
Contributor

pushrax commented May 17, 2014

@iamjem yep, exactly.

@brendensoares
Copy link
Member

@landr0id @pushrax thanks for bring that up and the issue link, too. I knew UUID was a recent conversation, but couldn't recall where we discussed it. I think we all agree that this change is needed.

@verdverm
Copy link
Contributor

Does this handle per-page CSRF tokens?
I'm currently using github.com/cbonello/revel-csrf and I recall a comment on some issue about wanting per-page tokens

for example, what happens if I have multiple tabs open of the same form? or two different pages from the same site?

looks good, thanks?

@iamjem
Copy link
Contributor Author

iamjem commented May 19, 2014

Hadn't seen this project before, implementation-wise it looks pretty similar.

What is the use case for per-page tokens? I guess I don't see what the security concern would be there? If a user logs in at some point, you'd want to refresh the token (which there's a convenience function for).

On an unrelated note, I'm wondering if there's a more elegant way to handle exemptions than manually registering absolute paths as is done in this package.

@iamjem
Copy link
Contributor Author

iamjem commented May 20, 2014

Talked with another Revel enthusiast who had some more insight into some tweaks we should consider to improve security:

  • Remove SHA1/salt and just use the random string from crypto/rand we're generating. Using SHA1 may actually reduce randomness.
    • Sounds like a good idea. I can remove this and reduce complexity a bit more.
  • Prevent tokens from leaking in GET requests.
    • I'm assuming this is when a form uses GET instead of POST, and the token shows up in the URL? Security risk being that its now visible in the URL.. and could be captured in HTTP Referer if the user left the site? I'm not sure we could prevent the token from showing in the query string, but we could perhaps refresh the token when this happens so the token in the query string is now invalid?

@tomsteele
Copy link

@iamjem for the leaking. I was referring to any sort of CORS functionality that may be in use in conjunction with this feature. If we have a route that supports CORS, we wouldn't want to return the token in a form/header for that request. Because then, in theory, an attacker would be able to grab a valid token from the response cross-site and possibly submit requests for non CORS routes.

Additionally, we should make sure that any sort of method override functionality transforms the the logical method prior to passing to this. Here is a good post where this has occurred in another framework http://blog.nodesecurity.io/post/60555138201/bypass-connect-csrf-protection-by-abusing.

@iamjem
Copy link
Contributor Author

iamjem commented May 21, 2014

Hmm thats interesting. As far as I know there's no way of doing anything similar to what the methodOverride middleware in Connect does within Revel. And really, it sounds like a dangerous feature to have period!

For the CORS it looks like we just need to ensure the scheme and host match for the request and referrer. Right now its only checking the scheme.

@tomsteele
Copy link

For CORS it should be ok to check for the existence of the "Origin" header and if present, not populate the token.

@ghost ghost mentioned this pull request Aug 20, 2014
@brendensoares
Copy link
Member

I want to get this merged ASAP. Mad rush to start confirming v0.11's PR's. Are we good to merge this into develop?

@iamjem
Copy link
Contributor Author

iamjem commented Sep 11, 2014

I'll look through this once more and make sure everyone's comments were
incorporated. Its been a while and this isn't terribly fresh in my head!
Might have to be this weekend though.

On Thu, Sep 11, 2014 at 2:20 PM, Brenden Soares notifications@github.com
wrote:

I want to get this merged ASAP. Mad rush to start confirming v0.11's PR's.
Are we good to merge this into develop?


Reply to this email directly or view it on GitHub
#606 (comment).

@brendensoares
Copy link
Member

TODO

  • remove sha1/salt in favor of crypt/rand (this seems like a negligible benefit)
  • support per page tokens (which could be a later enhancement)
  • Check and ignore request with origin header

@iamjem when can you get this done? Thanks!

"encoding/base64"
"encoding/hex"
"fmt"
"github.com/revel/revel"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the revel import down to its own section. We're trying to separate the imports into "built-in" and "other" sections.

@brendensoares
Copy link
Member

@iamjem can we get this done in a day or so? If not, I'll merge this and then finish up the TODOs as noted above.

Thanks for your help so far!

@iamjem
Copy link
Contributor Author

iamjem commented Oct 7, 2014

@brendensoares sorry for my absence, made some updates, i guess i had some laying around for a while back that hadn't been pushed either. take a look.

the one thing i'm still missing is CSRF exempt. really think it should be convenient for a user.. the existing CSRF library for revel makes you manually register route paths, which I think blows. probably a way to mark an entire controller as CSRF exempt, but that has implications code-wise for the user. any ideas?

in python and node this is normally wrapping a method/function in a special CSRF exempt decorator, and you're done.

@ghost ghost mentioned this pull request Oct 8, 2014
@brendensoares
Copy link
Member

@AnonX Uniform Resource Name (had to Google that). Either way, using the controller action may be a cleaner/easier solution, but what if an action is targeted by multiple routes? Probably not common enough to care, but curious enough to ask.

@ghost
Copy link

ghost commented Oct 8, 2014

G'day, @brendensoares. Why would somebody want to exempt actionX on urn1 but have CSRF enabled for the same actionX on urn2?

@iamjem
Copy link
Contributor Author

iamjem commented Oct 8, 2014

@brendensoares @AnonX at the time the CSRF filter is running, does it know what the target controller method is? I agree, registering a URL explicitly is duplicative since its already in the routes.

And for the controller action duplication, I think thats a valid point. Someone might have the same controller action registered for different HTTP methods, or even different nearly identical routes, and have conditional logic within it based on which method?

@brendensoares
Copy link
Member

@AnonX @iamjem I suppose we could just suggest the best practice of not using the same action for different routes. It does seem to be a rare edge case.

@iamjem can you add the csrf.MarkExempt() method to use the controller action? Perhaps we could even abstract the func getRedirectUrl(item interface{}) (string, error) functionality into a separate function so that it and the new csrf method can accept either a string route URL or a controller action.

@iamjem
Copy link
Contributor Author

iamjem commented Oct 9, 2014

Just a status update, should have a first stab at things done tonight for review tomorrow. Planning on accepting string paths, compiled regex, and method expressions to the MarkExempt method.

@iamjem
Copy link
Contributor Author

iamjem commented Oct 10, 2014

Few stumbling points I've ran into.

First off, I'm not finding a way to reflect a method expression to get its method name? You can reflect the function's args of course, the first of which would be the controller struct. But I would need to do some validation of the method expression to ensure its a valid struct (that contains an embedded revel.Controller), and then piece together the controller name and action to compare against controller.Action?

type MyController struct {
    *revel.Controller
}

func (m MyController) Index() revel.Result {}

csrf.MarkExempt(MyController.Index)
// or this should work too
csrf.MarkExempt((*MyController).Index)

Or is this even worth it?

Second topic is concurrency. I would like to avoid having to use synchronization on the maps/slices that hold these exempt values since these filters would be getting called on every request, and that could have performance implications. I believe this would only be safe if values were only being read from once the server is accepting requests. I would imagine that a developer would typically mark exempt methods in the various init hooks throughout their app before and not while the server is running. Is that a safe assumption?

@atomical
Copy link

What's the advantage of options as a map instead of a struct?

requestToken = c.Request.Header.Get("X-CSRFToken")
}

if requestToken == "" || !compareToken(requestToken, csrfSecret) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would make more sense if this if statement (line 85) was inside the previous if statement block (line 81). Otherwise it appears like your checking the same thing twice.

Leave as is, code is correct as stands

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid point that would improve code readability.

@ghost
Copy link

ghost commented Oct 12, 2014

@iamjem

I would need to do some validation of the method expression to ensure its a valid struct (that contains an embedded revel.Controller), and then piece together the controller name and action to compare against controller.Action?

To my view of thinking, it's a good idea to keep everything idiot-proof and make sure that App.Index argument of csrf.MarkExempt(App.Index) is a correct action that returns revel.Result and App is a struct that contains revel.Controller. But that is not absolutely necessary.

Nothing bad will happen if I pass csrf.MarkExempt(NotController.SmthToString), right? As this is not an action, project users will never visit /NotController/SmthToString, so no need to exempt it and worry about it.

getRedirectUrl till the line 355 is enough to eliminate typos in input parameters and throw errors in case structures are renamed but changes are not reflected in MarkExempt part.

I would imagine that a developer would typically mark exempt methods in the various init hooks throughout their app before and not while the server is running. Is that a safe assumption?

I think, yes. It should be called from revel.OnAppStart(func() { ... }) most of the time. Exempting actions on the fly looks like an extremely rare use case to make every app lose in performance.

What do you think, @brendensoares?

@iamjem
Copy link
Contributor Author

iamjem commented Oct 12, 2014

Keep in mind too one of my questions: is it possible to reflect the name of the method in a method expression? My earlier attempts weren't working. I could figure out the controller no problem, but whatever magic happens when you use a method expression seemed to hide the original methods name. Not a type I've personally had to introspect before.

@ghost
Copy link

ghost commented Oct 13, 2014

@iamjem, there is a revel.FindMethod which iterates through all methods of a certain controller and returns a method you need which has Name attribute. Or do you mean something else? If so could you please provide an example?

@iamjem
Copy link
Contributor Author

iamjem commented Oct 13, 2014

@AnonX my question is, can we use reflection to determine the name of the method in a method expression? I haven't had any luck doing so. Code wise, this is what I'm expecting csrf.MarkExempt(item interface{}) should do:

func MarkExempt(item interface{}) {
    // First check and handle when item is a string (URI), regex (code omitted)
    // Now check if item is a function (in the case of a method expression)
    if reflect.TypeOf(item).Kind() == reflect.Func {
        // Now we can inspect item's arguments, the first of which
        // should be a controller.. but how do we know what the name of
        // the method on the controller is that we care about? We need it for
        // validation later, when its compared against *controller.Action...
        // This (along with other things I've tried) doesn't return the method name...
        // We basically want to find "Index" if someone did MarkExempt(SomeController.Index)
        fmt.Println(reflect.TypeOf(item).Name())
    }
}

// Sample controller
type HomeController {
    *revel.Controller
}

func (h HomeController) Index() revel.Result {}

// Example of marking it exempt in the app init:
csrf.MarkExempt(HomeController.Index)
// or
csrf.MarkExempt((*HomeController).Index)
// or we say forget it, and they just use a string
csrf.MarkExempt("HomeController.Index")

@brendensoares
Copy link
Member

@AnonX @iamjem I'm on IRC, can we chat really fast?

@iamjem
Copy link
Contributor Author

iamjem commented Oct 14, 2014

Believe I have the couple code cleanup items, exemptions, and tests committed now. After talking with @brendensoares on IRC, we decided to go with exemptions for paths and controller actions (as strings). So, to mark exempt, you'd do something like this:

// as a path
csrf.MarkExempt("/ControllerName/ActionName")
// as a controller action, validated against controller.Action
csrf.MarkExempt("ControllerName.ActionName")

Some possible future updates could include accepting method expressions, and potentially regular expressions.

@brendensoares
Copy link
Member

👍 Can't wait tp get this released. Much anticopated :)

@brendensoares
Copy link
Member

@iamjem Can we merge this in the next day or so?

@iamjem
Copy link
Contributor Author

iamjem commented Oct 22, 2014

@brendensoares what do you need from me?

@brendensoares
Copy link
Member

@iamjem last time we chatted you said you had some buttoning up to do for this PR. Is it ready to be merged?

@iamjem
Copy link
Contributor Author

iamjem commented Oct 23, 2014

My last commit should have covered everything in that regard. I think its
good to be merged in.

On Wed, Oct 22, 2014 at 9:56 AM, Brenden Soares notifications@github.com
wrote:

@iamjem https://github.com/iamjem last time we chatted you said you had
some buttoning up to do for this PR. Is it ready to be merged?


Reply to this email directly or view it on GitHub
#606 (comment).

@brendensoares
Copy link
Member

@iamjem Great! I'll do a final review and test tonight.

@brendensoares
Copy link
Member

I'll merge this and then add your usage instructions as comments to the filter. Thanks @iamjem!

brendensoares added a commit that referenced this pull request Oct 24, 2014
@brendensoares brendensoares merged commit 48cc8bc into revel:develop Oct 24, 2014
// If the Request method isn't in the white listed methods
if !allowedMethods[c.Request.Method] && !IsExempt(c) {
// Token wasn't present at all
if !foundToken {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, can foundToken be ever false on line 53? Haven't we already checked it on line 43 and called RefreshToken(c)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AnonX yes, if the token wasn't sent in the form (right?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants