[2.2] [Session] Mixed-mode sessions #3247

Open
webmozart opened this Issue Feb 2, 2012 · 36 comments

10 participants

@webmozart
Symfony member

Now that the sessions are being reworked by @drak, I would like to introduce another feature which in my opinion is critical for secure websites: Secure session attributes.

How can sessions be hijacked in mixed HTTP/HTTPS environments?
  1. Alice makes a HTTP request for page A and obtains the session ID X
  2. Alice makes a HTTPS request for page B
  3. The website is programmed correctly and asks for reauthentication
  4. Alice authenticates and obtains a new session ID Y
  5. Alice makes another HTTP request for page A, this time transmitting Y in a cleartext session cookie
  6. Mallory intercepts the request
  7. Mallory makes a HTTPS request for page B using session ID Y and does evil stuff
Solution A: Secure session cookie

The only possible solution right now is to set session.cookie_secure to on. But then HTTP pages cannot access the session.

Solution B (proposed): Split sessions

The second idea is to split the session into an insecure and a secure area like this:

+---------------------------+
| Insecure attributes       |
| +-------------------+     |
| | Secure attributes |     |
| +-------------------+     |
+---------------------------+

This is intended for the case when session.cookie_secure is off. The session cookie is always transmitted, so both HTTP and HTTPS pages can access it.

Within the session, a nested bag is stored that contains the secure attributes. In order to gain access to this secure bag, a key is needed. This key is transmitted in a second cookie, the "security cookie", with secure set to on (i.e. transmitted via SSL only).

Attributes in the outer container (=normal session) can be accessed

  • on HTTP and HTTPS pages
  • by hackers who hijack your session (easy, think public WiFi)

Attributes in the secure bag can be accessed

  • on HTTPS pages only and
  • if the value of the security cookie is correct
  • by hackers who hijack your session and intercept your SSL only security cookie, which is much harder

Developers can chose to

  • store data in the normal session if it is insensitive and must be accessible on HTTP pages
  • store data in the secure bag if it is sensitive and must never be accessible on HTTP pages
API
// Accessible on all pages
$session->set('cart_id', $cart->getId());

// Writable/readable only if HTTPS, throws an exception otherwise
$session->getSecureBag()->set('client_data_id', $clientData->getId());
Implementation

The value of the security cookie could be calculated as a hash of

  • microtime(true)
  • session_id()
  • a secret stored in the app configuration

So the session could look like this:

array(
    'cart_id' => 123,
    ...
    '__secure__fa28e1....ff07' => array(
        'client_data_id' => 52,
    ),
)

If you erase the security cookie (or if a hacker hijacks your session and sends the wrong security cookie), a new secure bag will be created, leaving the old one untouched.

+---------------------------+
| Insecure attributes       |
| +-------------------+     |
| | Your secure bag   |     |                         
| +-------------------+     |
| +-------------------+     |
| | Hacker's sec. bag |     |
| +-------------------+     |
+---------------------------+
How this fixes our initial example
  1. Alice makes a HTTP request for page A and obtains the session ID X
  2. Alice makes a HTTPS request for page B and obtains the hash J in the security cookie
  3. The website is programmed correctly and asks for reauthentication
  4. Alice authenticates and obtains a new session ID Y. The security hash J could be regenerated too, not sure this is necessary.
  5. Alice makes another HTTP request for page A, transmitting Y in a cleartext session cookie. J is not transmitted as the cookie is SSL-only.
  6. Mallory intercepts the request
  7. Mallory makes a HTTPS request for page B using session ID Y
  8. Since Mallory did not send a secure session hash, a new hash K is generated and sent back in the security cookie. Mallory cannot access attributes stored in the namespace J. Mallory can still access all other session attributes.
@webmozart
Symfony member

See also #2853

@ghost

I'm not entirely sure I understand what vector this closes yup? Session attributes are stored server side and never transmitted over the transport - only the session id is transmitted over the transport protocol, in a cookie. Could you explain the exact vector please?

@webmozart
Symfony member

You already named it. The cookie can be intercepted and faked, which gives me the possibility to hijack the session. The secure cookie, on the other hand, can't be intercepted.

@ghost

I am sorry but I still don't see that. A session cookie simple contains an ID as it's data. No other data or attributes are stored. The session attributes are only stored server side. Of course, anyone can write a cookie and change the session ID regardless of the transport, but you would have to know the ID of the session in order to hijack it. Simply running a site in https solves that already. Normal cookies however should contain a signature which will prevent cookies being manipulated - for that the contents needs to be signed and stored with the cookie - that's an attack vector since data is being transmitted in the cookie payload. With sessions there is zero data, simply the ID.

@webmozart
Symfony member

Of course data is stored server side. Ultimately the problem is caused by many websites running some pages using HTTP and others using HTTPS.

Imagine I visit page A using HTTPS and get a session cookie X. The page displays sensitive information I entered previously. Now I visit page B, which uses HTTP, so suddenly cookie X is transmitted in cleartext and can be intercepted.

A malicious user can copy and reproduce the cookie X locally and visit page A using HTTPS to access my sensitive information. The server can't verify that the malicious user is not me, because the cookie value is the same, and the IP, user agent string and other potentially identifying information can be faked.

@cboden

@bschussek Would the security aspect not be covered by session_set_cookie_params? Roughly translated in Symfony to:

<?php
    $sess = new Session(new NativeFileSessionStorage(null, array('cookie_secure')));

Or are you looking for additional functionality along with security?

@webmozart
Symfony member

@cboden No, because in your case AFAIK the session is HTTPS-only. I was proposing a solution for mixed HTTP/HTTPS environments

@ghost

I still don't understand how this brings overall security as this ticket assumes that only sensitive data might be stored and retrieved from session attributes, which is clearly not the case - sensitive data will surely be persisted elsewhere too. The only secure way to protect that data is to ensure it's it retrievable only by https protocol and that can be done using the routing mechanism. Otherwise all this ticket ensures is that certain attributes might not be accessible over http yet let a sensitive database entry be displayed, e.g. someone's health records.

@webmozart
Symfony member

@drak We are talking about two orthogonal things.

Of course, sensitive information can be stored elsewhere. Still, the key to look up this sensitive information (like the customer's ID) is usually stored in the session.

Having two separate session namespaces protects developers (or teams) from their own errors. Once I consciously decide that some information, e.g. customerId, should be stored in the sensitive namespace, it can only be read on pages using https. If my coworker develops a different, insecure page, e.g. the "About" page of the website, and would like to display some customer information, he can't. He has to talk to you and make a conscious decision:

  1. change the "About"-page to be https-only (what you talked about before)
  2. change the security policy of the page and move customerId from the secure to the insecure session namespace

My point lies in the conscious decision that is required. Right now, things like this happen, sensitive data is being leaked on insecure pages without anyone taking notice.

@sstok

I can see your point, basically having two sessions.

The Secure one is only available when HTTPS is used, the other one is always present.
When switching to HTTPS the previous variables are imported into a new session, no?

Its similar to a plain-to-secure transition but this time preserving the previous session?
Me only concern is having two session-ids at the same time making this overkill.

@webmozart
Symfony member

There is no import involved. You, the developer, decide for each attribute whether to access it via the getAttributes() or via the getSecureAttributes() bag. There are also no two sessions. Secure attributes are simply stored inside the original session, but accessible via a hash key that is stored in a SSL only cookie.

Note that this doesn't prevent a developer completely from accessing secure attributes. If you read all attributes from the session and iterate over them, this will obviously include the secure attributes. But neither is this a standard use case. I think this wouldn't be a big problem, but something to be documented and to be aware of when doing such things.

@cboden

@bschussek Could this functionality be achieved by composition without editing the Session file?:

<?php
    // bootstrap - (assuming $request is an HttpFoundation Request object)
    if ($request->isSecure()) {
        $secure = new AttributeBag('secure');
        $session->registerBag($secure);
    }

    // later in script
    if ($loggedIn) {
        $session->getBag('secure')->set('user_id', $user->getId());
    }
@webmozart
Symfony member

I added a small example in the description in order to highlight why I think this feature is strongly necessary. Please correct me if there are better ways to solve this issue.

@snc
snc commented May 4, 2012

It would even be possible to persist the secure session attributes encrypted, that would prevent the all issue you mentioned...

@schmittjoh

If an attacker obtains your session cookie (via an insecure site), what prevents him from accessing a secure site with your cookie, and then consequentially accessing the secure attributes as well?

@snc
snc commented May 4, 2012

@schmittjoh if I understand @bschussek right, the call to getSecureAttributes() will fail or return no data because the value of the secure cookie has not been sent to the server...

@webmozart
Symfony member

@snc Exactly. (on both points)

@webmozart
Symfony member

I extended the description above again for an example (both before and after introducing this feature).

@schmittjoh

Ok, so we basically have two cookies (the session cookie for HTTP/HTTPS, and an additional cookie HTTPS only).

Sounds good to me.

I would like to suggest a small change though. Namely, not adding getSecureAttribute and variants, but rather a secure parameter to the "set" method (set($name, $value, $secure = true/false)). For several reasons:

  • IMO it would be confusing to store the same attribute name in both the unsecure, and the secure namespace.
  • Someone accessing an attribute from the session does not have to be aware of whether this attribute is secure or not.
  • It would allow to switch attributes from secure to unsecure or vice-versa more easily, or deactivate the feature if you don't want to use it.
@sstok

Ah, that makes it clear. Great plan!
+1 for me.

You can make a shopping site where the shopping section and your basket is HTTP and the checkout section is HTTPS.
You can always see your basket but still shop in a none-HTTPS version. So there are valid use cases for this.

Also like the idea of @schmittjoh of using an parameter, this makes it almost the same as setcookie where you have the secure parameter.

@webmozart
Symfony member

@schmittjoh You suggest adding the parameter only to set, but not to get? How does get determine whether to look for a secure or a non-secure attribute in this case?

@schmittjoh
@webmozart
Symfony member

I see. Fine by me in that case. I think we should definitely throw an exception though.

Apart from that we should look into implementing this in a way such that secure attributes can optionally be encrypted too if the developer desires that.

@ghost

I have a very simple answer to give here but before I do, since a lot of other topics have been talked about, I would like to explain some stuff about session security. I know most of you know more about this topic than me but I want to address these points so we are all on the same page.

Firstly the concept of encryption of a session. This has two parts. The session data and the session ID which is passed by the browser back to the server are separate. At no point is the session data ever exposed to an attacker except where data is deliberately output in the view. So the very concept of mixing encrypted and non-encrypted session data is strange and unnecessary.

The decision to encrypt back-end storage an application implementation detail. If one was storing credit card data you would/should encrypt that data so if someone got physical access or hacked the server the data would not be exposed. However, were talking about getting direct access to the database here - either via SQL injection which means your application is broken anyway, or at a completely different level, i.e. server filesystem/login level - at which point security is completely breached.

If one wishes to encrypt the session then that is what the proxy mechanism is for and I already documented it at php.net here: http://docs.php.net/SessionHandler (see Example #1). This reminds me I was supposed to add some example code to the documentation at symfony.com. Please see the Symfony docs on the proxy mechanism.

The next thing I read here was something about "if an attacker got your session ID" as if it's easy - but if someone does get your session ID, then the game is over (unless we do other things I will explain in a minute).

Getting a session ID is extremely difficult unless you perform man in the middle attacks on non-http connections or the application does something stupid (like expose the session ID in the URL or have session.trans_id = 1). The cure for man-in-the-middle is to use HTTPS, and that's done via routing. Plain and simple, if an application has secure areas, they should only be accessible by HTTPS.

The next is "Session Fixation" where you fix other details to the session ID - for example by client IP, IP subnet - that way the session can only be accessed by the IP or subnet that created it/logged in. You can also hash some details from the client browser signature (name, version, etc). This one is a very good protection because there are so many different browser versions and attacker would need to not only get your session ID but also your browser and IP metrics, and be able to connect from your IP or subnet.

Next, obviously one's application needs to be free from XSS by correct output filtering but Javascript has SOP so the session ID not available to 3rd party sites running JS on the same page - in any case, XSS is a risk and needs to be fixed there.

These are all things that can (and should) be done at application level. We do this in Zikula sessions for example - sessions can be fixed to IP, IP subnet, and browser signature. OT, we also have cookie signing, so a client cannot interfere with a cookie - this is not relevant with session cookies since there is nothing to forge (there is no data except the session ID). I do have that on my list of PRs to-do for Symfony Sessions.

I will give my reply in the next comment which addresses the concerns of @bschussek and shows why there is no security issue or attack vector as described.

@ghost

So the concern of this ticket is that a user may authenticate via https but later use http and their client will still send their session cookie ID over a non-secure connection. If session.cookie_secure is set to 1, this will not occur as the client browser respects the cookie setting and will not transmit a cookie marked as secure over an HTTP connection.

I have made a gist to demonstrate this: https://gist.github.com/2620468

Please clear your browser cookies for localhost before running the script.

The code shows the cookies received by the client browser and you can clearly see that the browser does not send the secure cookies over HTTP requests.

From the PHP script side, it is important, where it matters, not to create new cookies we want to be secure, over a non secure connection. This would mean checking the http request environment before issuing a cookie. Again, this could be done with routing because a login should always be over HTTPS anyway and if it is, the transmission of the cookie is over HTTPS back to the client so this is covered. There should be no HTTP-only route to a login page.

If one is protecting against MITM attack, then even though the session cookie is only transmitted once for the lifetime of the cookie from server to client, it should not be done in plain-text, but sent over SSL.

@webmozart
Symfony member

@drak I'm aware of that and listed your solution even in the description under "Solution A". Would you mind to re-read?

@snc
snc commented May 6, 2012

If you want to encrypt all sessions transparently you can use the suhosin extension if available. But this is a little bit off topic...

@chx

Drupal does not split the session it is a little bit trickier what it does.

You get two session cookies, your insecure session ID which is transmitted over both HTTP and HTTPS and your secure session ID which is HTTPS only.

When reading your session the following scenarios are considered:

  1. HTTP request, we use your insecure cookie only.
  2. HTTPS request, we try to fetch your session using your secure session ID.
  3. HTTPS request, we try to fetch an anonymous session using your insecure session ID.

The results is that stealing the insecure cookie is useless: on a properly configured site will never let you execute anything privileged.

@webmozart
Symfony member

Thank you for the input @chx. Sounds very reasonable.

ping @fabpot

@lu4e3ar

An ability for client to have 2 unrelated sessions(secure & insecure, like in @chx description) looks weird.
For example, in this case user has an ability to browse site with 2 different logins with unclear login switches during protocol changes.

@bschussek proposal about HTTPS-only attributes bag sounds good for me because user will have only one session.

@stof
Symfony member

@fabpot what do you think about this feature ?

@chx

For example, in this case user has an ability to browse site with 2 different logins with unclear login switches during protocol changes.

If you use this split mode then don't allow for HTTP logins. HTTP is for anonymous only.

@znerol

The Drupal use-case is a little bit different but still somewhat related to what was proposed here.

Let me illustrate this with the following example: Consider a typical company website with a busy public front page and an extranet for business partners. In order to prevent trivial drive-by industry espionage it was decided that the extranet only should be reachable via HTTPS. At the same time it seemed to make little sense to also enforce encrypted connections on public parts of the website.

Thus there is a set of paths where HTTPS always should be used and maybe another set of paths where HTTPS is not desired (for performance reasons, but I consider that a mere historical argument).

When mixed mode SSL is turned on in Drupal, the results are:

  • Alice always accesses the same session data, no matter if she browses the public HTTP section or the private HTTPS-only extranet.
  • She only has to log-in once (via HTTPS) and the session will be resumed even if she closes the browser and opens it again starting at the insecure public front page.
  • If Mallory manages to obtain the insecure session cookie (e.g. from the open wifi guest network of the company), she can impersonate Alice on the public parts of the website. But still it is impossible for Mallory to get into the extranet.

Mixed mode SSL as implemented in Drupal is to prevent against simple drive-by attacks. It does not help against active man-in-the-middle (e.g. SSLstrip) though.

Also note that by default Drupal sets session.cookie_secure to on and also uses a different session-name if the request came in via HTTPS. This means that in a standard Drupal installation, users will work with two different sessions when protocols are mixed. Although this is the most secure configuration it is also very confusing for novices.

@webmozart
Symfony member

Thank you for the insight @znerol. What I propose above is effectively what you describe.

@webmozart webmozart changed the title from [2.2] [Session] Secure session attributes to [2.2] [Session] Mixed mode sessions Sep 1, 2014
@webmozart webmozart changed the title from [2.2] [Session] Mixed mode sessions to [2.2] [Session] Mixed-mode sessions Sep 1, 2014
@hbedrosian

@webmozart Has there been any progress on this proposal or specific plans to implement it? Support within the framework for mixed mode session handling would solve a real PITA issue so I'm certainly in favor of it. 👍

@znerol

For the record, mixed mode session support has been removed from Drupal 8 core.

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