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
Protect against Session Fixation and session data leakage when crossing privilege boundaries #1570
Conversation
I think we may wish to backport this to 1.5-branch as well. I'm +1 on this PR and will merge it after it has baked another day or so for anyone else to comment. |
A more comprehensive change, but one with two possible backwards incompatibilities for users would be changing this to: def remember(self, request, userid, **kw):
""" Store a userid in the session."""
# To protect against session fixation attacks we want to ensure that
# we have a new session identifier (where applicable) when remembering
# a user authentication.
if (self.userid_key in request.session
and request.session[self.userid_key] != userid):
# If we already have an authenticated user, we're going to just
# invalidate the session without copying data. We'll do this
# because going authenticated -> authenticated might leak sensitive
# information and we don't want to re-use another user's session.
request.session.invalidate()
else:
# We don't already have an authenticated user, or if we do it's
# the same user we're trying to remember. Since this is either
# already this user's session or we're going from
# unauthenticated -> authenticated we'll just copy the data from
# the session, invalidate it to create a new session, and restore
# the data.
data = request.session.copy()
request.session.invalidate()
request.session.update(data)
# Actually store the user in the session to remember which user has
# been authenticated.
request.session[self.userid_key] = userid
return []
def forget(self, request):
""" Remove the stored userid from the session."""
# When crossing an authentication boundry we want to create a new
# session identifier. We don't want to keep any information in the
# session when going from authenticated to unauthenticated because
# user's generally expect that logging out is a desctructive action
# that erases all of their private data. However if we don't clear the
# session then another user can use the computer after them, log in to
# their account, and then gain access to anything sensitive stored in
# the session for the original user.
request.session.invalidate()
return [] This will make sure that:
1 and 2 are more or less backwards compatible as long as no one is relying on retaining the same session identifier across If you'd prefer to get the more complete fix I can update this PR to do that, or I can open a new PR. |
# should ensure we have a completely new session, ideally with a new | ||
# session identifier where applicable, which will protect against a | ||
# fixated session crossing authentication boundaries. | ||
data = request.session.copy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ISession
extends IDict
and neither has an explicit requirement that they have a .copy()
function. It also looks like IDict
was originally only intended for renderers, looking at the docstring on IDict.update(d)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I can switch it to using data = dict(request.session.items())
.
Updated with the more comprehensive changes above, and to fix the reliance on a method that |
I am a +1 on this PR as is. From a security standpoint, I would +1 your extended fix. It might be backwards incompatible, but if anyone is relying on that I want to know what apps they are writing ;-) |
I thought you'd be implementing this more generally in |
I implemented it in On the other hand now that this pull request also covers attempting to stop data leakage via the session that can occur when crossing privilege boundaries you're right in that it really is more general than just session fixation and it would be appropriate to move it out of the |
You can't have the top-level API's doing anything with a session because there is no guarantee that the user has registered a session or is using one. Also, in my apps I have the authentication be a separate cookie from the session, so these protections are not required. |
I'm not sure it's not required. You can still leak data between authenticated and unauthenticated if you're storing any data in the session. |
Sure, you can, it all depends on what type of data you store on the session. I don't agree with unilaterally clearing out the session in the authentication API's. I would find it a very hard pill to swallow that my session would get cleared upon remember/forget when I am not using an authentication policy that uses session to store it's information. |
Ugh, I just realised that if we don't add it to the authentication API's essentially anyone writing an authentication policy would have to write the logic themselves. I can see the merit... I withdraw my previous complaints, and would +1 such a change. If a session policy is enabled, let's go ahead and clear it. |
What's the best way to determine if a session policy is enabled? Should I just try to access |
@dstufft ~~The way I would do it would be: from pyramid.interfaces import ISessionFactory
session_factory = request.registry.queryUtility(ISessionFactory)
Whilst the above is correct, it's overly complex. and not necessary. |
7fc52da
to
94a2b20
Compare
Ok, I've reworked this so that it's implemented inside of |
LGTM! |
``pyramid.security.forget`` invalidate the old session when a session factory | ||
has been configured. However, ensure that session data is kept intact when | ||
calling ``pyramid.security.remember()`` when the previous userid was either | ||
``None`` or the same as the userid that is being remembered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add the PR link here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added!
ship it! |
94a2b20
to
159a720
Compare
LGTM |
Just following up on whether this is still waiting for more eyes or if it can be merged. |
The reason I can't merge this atm is a hairy one. The |
Makes sense. Technically though they have whatever side effects the |
I think this Could patch could implement in |
It was originally, if you read up through the comments you can see the rationale for moving out of there and to the high level functions. |
@dstufft doh, my bad 👓 |
Certainly this is worthwhile to add to the Update remember(request, 'raydeo', response=request.response)
# or
response = HTTPSeeOther(...)
remember(request, 'raydeo', response=response)
return response I like this but it may be lipstick on a pig or missing some other apis like this. |
So the idea then is that you'd gate this particular change on That's obviously a gap that the current PR doesn't have in what's protected, however I don't know the Pyramid ecosystem well enough to know if that is a significant gap and how bad the bw-compat break would be as this is currently written. |
159a720
to
10e7332
Compare
Is it the case that these protections are universally applicable? Is this mitigating the impact of session fixation? What if, say, I run my application on SSL with HSTS and secure cookies? Of course, there are always going to be possible vulnerabilities, but is it the responsibility of the auth system to worry about session fixation? I wonder if this isn't driven by the need of a particular application to have more paranoid security but lacking hooks into some auth module. Maybe subscribers for remember and forget events would be useful? I may be completely off base, but I just thought I'd toss out some questions that might help reason through this. |
I recommended adding some events to remember/forget, however @mmerickel did not seem particularly enthusiastic about that idea when I proposed this to him on IRC. As for adding the hooks, while you might not be able to add them directly on the remember/forget, you can of course add the code to your custom auth policy. |
The answer to that question is a little complex. They are not universally applicable if you consider situations where there is no session configured, however this PR is effectively a no-op if you don't have sessions enabled so that case doesn't really matter. So the question then becomes, are these universally applicable if you're using a session? For that the answer is more or less yes unless you're using sessions but not storing user specific data in them. The attacks this protects against are not particular effected by TLS or secure cookies. You can set a cookie via a HTTP connection (even secure cookies!) and the app hosted via TLS won't know the difference. It can be mitigated if your entire site (aka the root domain and all subdomains using the includeSubdomains flag) is protected by HSTS, however the protection isn't complete since HSTS relies on either getting pre-loaded or the user having visited your site within the max-age. This isn't really a "paranoid" security change, this is a fairly basic protection that protects against one the OWASP top 10 attacks (It was #2 on the 2013 list), it's the same sort of protection as something like having auto escaping turned on by default in templates. I can easily fix this in my own application, but it's something that I believe Pyramid should really do on it's own. It's a fiddly security bit that super easy to get wrong if you remember to do it at all. I've written this kind of code before and it took me roughly an hour to sit there and convince myself that I had the right security properties once I had already written the code, this sort of thing is exactly the kind of thing that I think a framework should provide, fiddly hard to get right code that most everyone is going to need. To be clear too, the impact of merging this is: Common Cases:
Uncommon Cases:
I personally feel like the negative impact of this are fairly small corner cases, and the positive impact is closing up a flaw that affects most web applications and can trivially be used to steal someone's user account. |
10e7332
to
f40f96f
Compare
Sorry, slightly wrong above, in the first uncommon case, authenticated to authenticated with the same user id there is no new session ID, this change is a strict no-op in that case. |
Thank you so much for the detailed response @dstufft. I defintely learned some things. I also just took a moment to convince myself that 👍 from me! |
Do you think it might be wise to call |
I don't care much if we call |
Fine to leave it out. No further comments. Thanks for being so patient. |
f40f96f
to
4c2e3df
Compare
Are we waiting on anything specific to merge this? |
Yeah my comments about the design of the API are the main blocker if you look at some of the previous comments. I didn't write the API but it's clear it's intended to have no side-effects from the way it is implemented and the way the auth policies work (their values are not cached, etc). This PR is adding a side-effect and thus not in the spirit of the API. We may be able to justify it though in terms of the security benefits and the fact that the side-effects are idempotent (you could call |
We're waiting on @mcdonc to make a decision about that right? |
His API design prowess would be much appreciated. :-) He gave a talk on API design at pycon, so you know he's good. |
I'm going to say -1 to making remember() or forget() have side effects, sorry. I'd suggest a couple of things:
|
@mcdonc What about moving it back into the |
The concern is that detecting the absence of the "make me safe" function is hard to notice, so if there's a place we can have it on by default, that would be ideal. |
I think maybe we should do a combination of those two things:
Then if someone happens to be using sessions but not the SessionAuthenticationPolicy and they do need to invalidate the session at boundaries, they can do it easily. |
Makes sense! Would we just document that you should do something like this: if (request.unauthenticated_userid is not None
and request.unauthenticated_userid != userid):
request.session.invalidate()
else:
request.clone_session() # Maybe this should be on session itself? IDK Or would we have I suppose we'd also want to document that you should explicitly invalidate the session when you log out the user too if you're not using the |
Maybe there are two methods added to the request:
Then the SessionAuthenticationPolicy calls some_other_method_name(userid) |
Er, have that backwards, but you know what I mean. |
FWIW, the reason I say that these methods should be added to the request and not to the session is because we'd need to chang the ISession interface to include the methods, and it would break any existing session impls (e.g. pyramid_redis_sessions, etc) |
And FWIW, if these methods are called when a session factory is not configured, I wouldn't try to protect the caller from seeing an exception. I'd just let the normal session-retrieval machinery raise whatever exception it already does. |
In a stunning reversal of opinion in IRC, we decided that we would not merge this PR at all. Instead, we'll add some docs about OWASP best practices to the cookbook or possibly to a "hardening" section of the Pyramid docs (see #1591 (comment)) |
This will invalidate the session in the
SessionAuthenticationPolicy().remember()
, ensuring that all the data is copied over to the new session. This will ensure that for server side sessions which have a session ID, a new session ID is granted when going from an unauthenticated user to an authenticated user. For client side sessions this will simply do a little extra work but ultimately be a no-op.Fixes #1569