-
Notifications
You must be signed in to change notification settings - Fork 34
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
Help me understand the CSRF protection - Update: it doesn't work properly! #23
Comments
OK, I have now been able to test the CSRF filter, and it's very problematic. Despite all the fussing about cookies, it is in fact nothing more than a referrer check.
|
@ThrawnCA thanks for seeing this code inspection through. We’re keen to work through with you on a fix for this. Ideally we’d like to see CSRF implemented in CKAN core but we’ll do what we can here in the meantime. |
Have you got a preferred implementation approach if we are to patch this issue @ThrawnCA? |
Well, the key issue is that the token cookie needs to be compared to some part of the request. Otherwise, the token's presence doesn't prove that the request is legitimate. I'm one of the maintainers of the Queensland Government CKAN extension, which uses a CSRF filter based on the OWASP Double Submit Cookie pattern. That has its own drawbacks, but we chose it because it's stateless and could be implemented with a very small footprint. The filter code is visible at https://github.com/qld-gov-au/ckan-ex-qgov/blob/master/ckanext/qgov/common/anti_csrf.py Essentially:
Ideally, the limitations of the Double Submit Cookie approach could be worked around by using aspects of the Encryption based Token pattern (but continuing to use cookies instead of relying on AJAX). If tokens were generated by encrypting user ID, timestamp, and nonce using the beaker session secret or other server-side key, then it would no longer be possible, for example, for an attacker to set legitimate-seeming cookies from alternative subdomains. (They still need to be compared to some aspect of the request, to prove that they originated from a HTML form issued by the server.) |
@ThrawnCA thanks very much for the code example also. We're keen in the short term to look at adopting the Double Submit Cookie pattern into this module... raises the question as to whether we can co-maintain that CSRF solution here as a more generic Security/CSRF extension for CKAN? Happy to collab on this if you are? Longer term we are keen to see how much security enhancements we can get baked into CKAN core (at which time we might opt to tackle elements of the Encryption based Token pattern you suggest. So seems like we have a bit of a roadmap forward here 👍 |
We're currently migrating our CKAN instances to new hosting, but after that, there is some interest at this end in making use of your extension, so there's probably room for collaboration at that point. There are a few aspects of our different filter approaches that are worth comparing and contrasting:
I suppose that if the Redis interaction could be adjusted to more easily coexist with CKAN core (and the Harvest extension if applicable), that might not be a significant overhead. Especially if the extension could default to reusing the CKAN core Redis URL instead of needing it to be repeated in the config file. Note that our Redis instance does not support multiple databases, so we'd rather use a unique prefix on the same database. On the other hand, encrypted tokens would resolve this without needing server-side state.
Using encrypted tokens containing a nonce and timestamp, instead of random ones, would give better options here. Suppose that we expect people to ordinarily take no more than 10 minutes to fill in a form. We could set a hard limit, eg allow tokens up to 30 minutes old, but at the same time, attempt to set a new token if the current token has less than ten minutes left. That way, we ensure that people have at least ten minutes to use their token, but we don't churn out tokens unnecessarily before that point. There would also be no need to invalidate old tokens on page load, so multiple tabs would work normally. |
Hi @ThrawnCA , thanks for raising this issue. On using the same redis instance:
Now there's no question that your implementation is an improvement on the one we've inherited, but we'd be keen on working together to understand it a bit better and potentially improve it. Thanks for giving us the link to the OWASP cheatsheet, that gives us a lot more context on your approach. Questions about the current implementation:
Notes:
After reading through and thinking on this issue a bit, I feel like using an implementation that caches the token on the server-side might be a valid final solution, but adopting your current, and most importantly working, solution would be a good intermediate step. Thoughts? |
Why do you need to create tokens for all public users? Do you allow them to take actions with side effects without registration? On our instances we haven't, so we only generate or require tokens when logged in. Edit: Ah, is that to prevent login CSRF? It should be sufficient to generate a token when an unauthenticated user actually requests the login form, and then requiring a token to log in. Thanks for bringing our attention to that; it's something we could improve.
Your explanation does make sense. I'd rather ditch server-side state altogether, if possible.
Actually I hadn't realised that that move was a thing. If it's just a different login cookie name, it should be easy enough to support both, yes? Our only interaction with auth_tkt is to check whether someone is logged in or not.
Ah. Actually I know for a fact that it does, in at least one remaining case that hasn't become enough of a priority to fix yet: deleting a member from an organisation. The reason for this is a JavaScript confirmation dialog that intercepts the link and assembles an empty POST aimed at the link target. Weird behaviour, if you ask me, but that's what it does. You might notice the special CONFIRM_LINK handling; that is a workaround for cases where the link has no query string. We haven't yet developed a workaround for the one existing case where it does have a query string, although it shouldn't be especially difficult. But in general, the HTML rewrite has been quite reliable. If you have suggestions to refine the POST_FORM regex, I'd be happy to consider them.
Addressed above; I'm pretty sure we could defend against this simply by adjusting the conditions for when to set and expect tokens.
Actually I came across your extension by accident :). I was looking up information about CSRF on the CKAN API (which, btw, is a serious problem), and my search terms found ckanext-security, which looked interesting because of its similarity to our own work. We do, in fact, use Strict Transport Security, although not with subdomains, and it should be difficult for an attacker to register a qld.gov.au subdomain, so Double Submit Cookie is relatively safe; however, it's true that it has weaknesses. Encrypted cookies should cover most of them. We actually have a Referer check, too, though implemented in Apache:
Actually, we wouldn't be storing sensitive data, we just need to stop any kind of tampering or forgery. So a signature should be sufficient. The cookie value would end up something like: admin!1554344057471!9999999!hmac-value-goes-here Hmm...OWASP seems to think that injecting such a token into the form might be enough without having a token cookie at all, although they would then add an extra field representing the operation to be performed. There may be something in that. Anyway, there's information about Python encryption at https://www.blog.pythonlibrary.org/2016/05/18/python-3-an-intro-to-encryption/ if you want to read up and get some ideas. |
The documentation of the Python I think we could get a lot of benefit from something like:
It doesn't matter that the nonce is a regular random value, rather than being cryptographically strong randomness, since the only point of it is to ensure that successive calls in the same second don't produce the same hash. This could be set and used much like the existing random tokens, but it would be impossible to forge and has expiry information built in. |
Hi @ThrawnCA , I've added the But that doesn't appear to be working. Do I potentially need to add the additional interfaces that are used by the Queensland plugin for your implementation to work correctly? |
How are you testing whether it works? Bear in mind that it won't take any action if you aren't logged in. If you are, then you should be able to see a The plugin interfaces shouldn't be needed for this; they're for other features. So long as something is calling the |
@ThrawnCA I'm not explicitly calling the init function, my assumption was that the CKAN framework would do that when using the plugin, that could be my problem. I've put my work so far in a branch here: |
Hmm...your function name looks right, and you're correct that it should be called automatically when the plugin is created by CKAN. What tests have you performed? |
I rebuilt the application then inspected the pages to see if I could find any hidden fields, or the appropriate cookie was being attached. As our current implementation gives all users sessions, I've hard-coded the value of the Potentially there was an issue with the rebuild, I'll continue to investigate. Thanks for confirming that the implementation looks about right, that gives me one less thing to investigate :) |
Ok, thanks for the help @ThrawnCA , I've managed to get it up and working, the following potential tasks remain:
Our implementation of brute-force protection requires us to continue to have sessions for all users, this also fixes not having login-protection, so I think I'll comment the hack and leave it in place for now. |
Good to hear it's working :). Re brute-force protection: Is that just to prevent brute-force logins? If so, then adjusting the |
I would like to shrink sessions down to just users who are visiting the login page/are already logged in if at all possible, considering how few admins we have compared to the number of views. I'll take a look at the beaker configuration while I'm here and see if we can work that out. Thanks for the suggestion @ThrawnCA . (Head's up, will probably be a bit unresponsive until Monday next week, ANZAC day tomorrow, Friday off, but will get on it first thing Monday :)) |
@anotheredward I've pushed a fix for the organisation member bug: ThrawnCA/ckanext-qgov@196bc32 |
@ThrawnCA Thanks for that patch! I'm integrating it now and should have an update PR out soon :) |
@anotheredward I've done some work on our extension to convert the tokens from random strings to HMACs. This makes it essentially impossible to forge a valid token without access to our Beaker session secret, thus eliminating the main weakness of the Double Submit cookies technique, and it also means that they can contain useful data such as a timestamp (which simplifies token rotation). The new token format is For reference: |
Thanks @ThrawnCA - @anotheredward has moved to a new role and @ebuckley is on the case. We'll look at making a tagged release of this module soon with the changes in #24 and then perhaps follow that up with some token enhancements as you've proposed 👍 |
Thanks for your contributions on this @ThrawnCA I have merged the PR and cut a release with our fix https://github.com/data-govt-nz/ckanext-security/releases/tag/1.1.0 |
I've taken a look at the CSRF protection in middleware.py, and I can't see how it actually protects against CSRF. It doesn't seem to be matching the cookie token against the contents of the request, so how would it stop an attack?
I'm going to see if I can get an instance set up and properly test it, but if someone can explain, that would be good.
The text was updated successfully, but these errors were encountered: