openedx.core.djangoapps.safe_sessions package#
Submodules#
openedx.core.djangoapps.safe_sessions.middleware module#
This module defines SafeSessionMiddleware that makes use of a SafeCookieData that cryptographically binds the user to the session id in the cookie.
The primary goal is to avoid and detect situations where a session is corrupted and the client becomes logged in as the wrong user. This could happen via cache corruption (which we’ve seen before) or a request handling bug. It’s unlikely to happen again, but would be a critical issue, so we’ve built in some checks to make sure the user on the session doesn’t change over the course of the session or between the request and response phases.
The secondary goal is to improve upon Django’s session handling by including cryptographically enforced expiration.
The implementation is inspired in part by the proposal in the paper <http://www.cse.msu.edu/~alexliu/publications/Cookie/cookie.pdf> but deviates in a number of ways; mostly it just uses the technique of an intermediate key for HMAC.
Note: The proposed protocol protects against replay attacks by use of channel binding—specifically, by incorporating the session key used in the SSL connection. However, this does not suit our needs since we want the ability to reuse the same cookie over multiple browser sessions, and in any case the server will be behind a load balancer and won’t have access to the correct SSL session information. So instead, we mitigate replay attacks by enforcing session cookie expiration (via TimestampSigner) and assuming SESSION_COOKIE_SECURE (see below).
We use django’s built-in Signer class, which makes use of a built-in salted_hmac function that derives an intermediate key from the server’s SECRET_KEY, as proposed in the paper.
Note: The paper proposes deriving an intermediate key from the session’s expiration time in order to protect against volume attacks. (Note that these hypothetical attacks would only succeed if HMAC-SHA1 were found to be weak, and there is presently no indication of this.) However, since django does not always use an expiration time, we instead use a random key salt to prevent volume attacks.
In fact, we actually use a specialized subclass of Signer called TimestampSigner. This signer binds a timestamp along with the signed data and verifies that the signature has not expired. We do this since django’s session stores do not actually verify the expiration of the session cookies. Django instead relies on the browser to honor session cookie expiration.
The resulting safe cookie data that gets stored as the value in the session cookie is:
version ‘|’ session_id ‘|’ key_salt ‘|’ signed_hash
where signed_hash is a structure incorporating the following value and a MAC (via TimestampSigner):
SHA256(version ‘|’ session_id ‘|’ user_id ‘|’)
TimestampSigner uses HMAC-SHA1 to derive a key from key_salt and the server’s SECRET_KEY; see django.core.signing for more details on the structure of the output (which takes the form of colon-delimited Base64.)
Note: We assume that the SESSION_COOKIE_SECURE setting is set to TRUE to prevent inadvertent leakage of the session cookie to a person-in-the-middle. The SESSION_COOKIE_SECURE flag indicates to the browser that the cookie should be sent only over an SSL-protected channel. Otherwise, a connection eavesdropper could copy the entire cookie and use it to impersonate the victim.
- Custom Attributes:
- safe_sessions.user_mismatch: ‘request-response-mismatch’ | ‘request-session-mismatch’
This attribute can be one of the above two values which correspond to the kind of comparison that failed when processing the response. See SafeSessionMiddleware._verify_user_and_log_mismatch
- class openedx.core.djangoapps.safe_sessions.middleware.EmailChangeMiddleware(get_response)#
Bases:
MiddlewareMixinMiddleware responsible for performing the following jobs on detecting an email change 1) It will update the session’s email and update the JWT cookie
to match the new email.
- It will invalidate any future session on other browsers where
the user’s email does not match its session email.
This middleware ensures that the sessions in other browsers are invalidated when a user changes their email in one browser. The active session in which the email change is made will remain valid.
The user’s email is stored in their session and JWT cookies during login and gets updated when the user changes their email. This middleware checks for any mismatch between the stored email and the current user’s email in each request, and if found, it invalidates/flushes the session and mark cookies for deletion in request which are then deleted in the process_response of SafeSessionMiddleware.
- process_request(request)#
Invalidate the user session if there’s a mismatch between the email in the user’s session and request.user.email.
- process_response(request, response)#
Update the logged-in cookies if the email change was requested
Store user’s email in session if not already
- static register_email_change(request, email)#
Stores the fact that an email change happened.
Sets the email in session for later comparison.
Sets a request level variable to mark that the user email change was requested.
- class openedx.core.djangoapps.safe_sessions.middleware.SafeCookieData(version, session_id, key_salt, signature)#
Bases:
objectCookie data that cryptographically binds and timestamps the user to the session id. It verifies the freshness of the cookie by checking its creation date using settings.SESSION_COOKIE_AGE.
- CURRENT_VERSION = '1'#
- SEPARATOR = '|'#
- classmethod create(session_id, user_id)#
Factory method for creating the cryptographically bound safe cookie data for the session and the user.
Raises SafeCookieError if session_id is None.
- classmethod parse(safe_cookie_string)#
Factory method that parses the serialized safe cookie data, verifies the version, and returns the safe cookie object.
Raises SafeCookieError if there are any issues parsing the safe_cookie_string.
- sign(user_id)#
Signs the safe cookie data for this user using a timestamped signature and an intermediate key derived from key_salt and server’s SECRET_KEY. Value under signature is the hexadecimal string from SHA256(version ‘|’ session_id ‘|’ user_id ‘|’).
- verify(user_id)#
Verifies the signature of this safe cookie data. Successful verification implies this cookie data is fresh (not expired) and bound to the given user.
- exception openedx.core.djangoapps.safe_sessions.middleware.SafeCookieError(error_message)#
Bases:
ExceptionAn exception class for safe cookie related errors.
- class openedx.core.djangoapps.safe_sessions.middleware.SafeSessionMiddleware(get_response)#
Bases:
SessionMiddleware,MiddlewareMixinA safer middleware implementation that uses SafeCookieData instead of just the session id to lookup and verify a user’s session.
- static get_user_id_from_session(request)#
Return the user_id stored in the session of the request.
- process_request(request)#
Processing the request is a multi-step process, as follows:
Step 1. The safe_cookie_data is parsed and verified from the session cookie.
Step 2. The session_id is retrieved from the safe_cookie_data and stored in place of the session cookie value, to be used by Django’s Session middleware.
Step 3. Call Django’s Session Middleware to find the session corresponding to the session_id and to set the session in the request.
Step 4. Once the session is retrieved, verify that the user bound in the safe_cookie_data matches the user attached to the server’s session information. Otherwise, reject the request (bypass the view and return an error or redirect).
Step 5. If all is successful, the now verified user_id is stored separately in the request object so it is available for another final verification before sending the response (in process_response).
- process_response(request, response)#
When creating a cookie for the response, a safe_cookie_data is created and put in place of the session_id in the session cookie.
Also, the session cookie is deleted if prior verification failed or the new cookie can’t be created for some reason.
Processing the response is a multi-step process, as follows:
Step 1. Call the parent’s method to (maybe) generate the basic cookie.
Step 2. Verify that the user marked at the time of process_request matches the user at this time when processing the response. If not, log the error.
Step 3. If a cookie is being sent with the response, update the cookie by replacing its session_id with a safe_cookie_data that binds the session and its corresponding user.
Step 4. Delete the cookie, if it’s marked for deletion.
- static set_user_id_in_session(request, user)#
Stores the user_id in the session of the request. Used by unit tests.
- static update_with_safe_session_cookie(cookies, user_id)#
Replaces the session_id in the session cookie with a freshly computed safe_cookie_data.
- openedx.core.djangoapps.safe_sessions.middleware.mark_user_change_as_expected(new_user_id)#
Indicate to the safe-sessions middleware that it is expected that the user is changing between the request and response phase of the current request.
The new_user_id may be None or an LMS user ID, and may be the same as the previous user ID.
- openedx.core.djangoapps.safe_sessions.middleware.obscure_token(value: str | None) str | None#
Return a short string that can be used to detect other occurrences of this string without revealing the original. Return None if value is None.
Outputs are intended to be transient and should not be stored or compared long-term, as they are dependent on the value of settings.SECRET_KEY, which can be rotated at any time.
WARNING: This code must only be used for high-entropy inputs that an attacker cannot enumerate, predict, or guess for other parties. In particular, it must not be used for sequential IDs or timestamps, since an attacker possessing the pepper could precompute the hashes. A non-cryptographic de-identification technique must be used in such cases, such as a lookup table.
- openedx.core.djangoapps.safe_sessions.middleware.track_request_user_changes(request)#
Instrument the request object so that we store changes to the user attribute for future logging if needed for debugging user mismatches. This is done by changing the __class__ attribute of the request object to point to a new class we created on the fly which is exactly the same as the underlying request class but with an override for the __setattr__ function to catch the attribute changes.