For secure (HTTPS) sites, Drupal 7 now stores the session identifier in the sessions.ssid column rather than sessions.sid. However, the sessions table still defines the sid column as the primary key.

When a user logs in on the secure site, the session is created with an empty string sid. No additional logins are possible until this session logs out, because merging in another row into the sessions table with empty string sid results in a duplicate primary key and the merge fails.

A possible fix is attached: always store the session identifier in the sid column, whether or not it is a secure session.

CommentFileSizeAuthor
#156 575280-156-empty-session-id.patch5.63 KBchx
#155 575280-155-empty-session-id.patch6.54 KBcarlos8f
#153 575280-empty-session-id-test-NO-FIX.patch3.83 KBcarlos8f
#153 575280-empty-session-id-test.patch6.93 KBcarlos8f
#147 575280-key_1.patch2.7 KBcarlos8f
#144 575280-key.patch1.29 KBmfb
#140 575280-key.patch663 bytesmfb
#132 575280-empty-session-id_23.patch20.29 KBcarlos8f
#130 575280-empty-session-id_22.patch20.24 KBcarlos8f
#129 575280-empty-session-id_21.patch20.38 KBcarlos8f
#127 575280-empty-session-id.patch20.36 KBmfb
#126 575280-empty-session-id_20.patch20.64 KBcarlos8f
#125 575280-empty-session-id_19.patch20.02 KBcarlos8f
#124 575280-empty-session-id_18.patch19.85 KBcarlos8f
#123 575280-empty-session-id_17.patch19.75 KBcarlos8f
#121 575280-empty-session-id_16.patch19.57 KBcarlos8f
#114 575280-empty-session-id.patch16.73 KBchx
#107 575280-empty-session-id.patch16.1 KBchx
#104 575280-empty-session-id.patch15.3 KBchx
#102 575280-empty-session-id.patch15.97 KBchx
#98 575280-empty-session-id_9.patch11.66 KBcarlos8f
#90 575280-empty-session-id.patch15.21 KBchx
#84 575280-empty-session-id.patch12.64 KBchx
#83 575280-empty-session-id_9.patch11.66 KBcarlos8f
#76 575280-empty-session-id.patch10.93 KBbleen
#69 575280-empty-session-id.patch10.93 KBmfb
#68 575280-empty-session-id.patch9.95 KBchx
#67 575280-empty-session-id.patch11.02 KBmfb
#66 575280-empty-session-id.patch10.29 KBmfb
#65 575280-empty-session-id.patch9.47 KBmfb
#63 575280-empty-session-id.patch6.95 KBmfb
#62 575280-empty-session-id.patch7.51 KBmfb
#58 575280-empty-session-id.patch5.22 KBmfb
#55 575280-empty-session-id.patch2.66 KBmfb
#34 https_session_exceptions.png24.72 KBdeekayen
#31 session.patch9.21 KBmfb
#26 session.patch8.65 KBmfb
#23 session.patch5.7 KBmfb
#21 session.patch5.12 KBmfb
#20 session.patch1.85 KBmfb
#11 session.patch1.93 KBmfb
#9 session.patch1.9 KBmfb
#6 session.patch1.57 KBmfb
session.patch794 bytesmfb
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Heine’s picture

Title: Only one session can be logged in at a time on secure sites » Impersonation when an httpS session exists.
Issue tags: +Release blocker, +Security

Additional information from Mark (send to the security team, but can & should be fixed publicly):

I found that running Drupal 7 on a HTTPS URL and logging in results in a session being saved to the sessions table with an empty string session identifier. Once this happens, if the site is also available on a HTTP URL, then anyone can pass an empty string session cookie to the HTTP URL e.g. curl --cookie SESS96efe38290907de812869e22ea7c7c62= http://... and make any request with the privileges of that initial HTTPS login.

Damien Tournoud’s picture

Status: Needs review » Needs work

I suggest we replace isset($_COOKIE[$insecure_session_name]) by !empty($_COOKIE[$insecure_session_name]) in _drupal_session_read(). The true bug is _drupal_session_read() now accept empty session ids ;)

Damien Tournoud’s picture

Title: Impersonation when an httpS session exists. » Impersonation when an https session exists.
mfb’s picture

Isn't the issue just that we are *writing* an empty session id? Drupal 6 would also happily read an empty session id if it were there.

mfb’s picture

By the way, I ran the test suite on my HTTPS instance, and found that the patch reduces the number of failures. But even with this patch I still had some failures in the session tests, which happened when the test framework seemed to mysteriously redirect from HTTPS to HTTP at certain times. This seems like an unrelated issue but thought I'd mention it in case anyone else is testing on HTTPS.

mfb’s picture

Status: Needs work » Needs review
FileSize
1.57 KB

OK I am also bundling into this patch a fix for issue noted in #5. Tests now pass in HTTPS.

Dries’s picture

Given that this is security related, some extra code comments would be nice.

Damien Tournoud’s picture

Status: Needs review » Needs work

We don't want the session to be accessible by HTTP when generated from HTTPS, so I'm not convinced the fix makes sense. The real bug is accepting an empty session id.

mfb’s picture

Status: Needs work » Needs review
FileSize
1.9 KB

I added some code comments.

We don't want the session to be accessible by HTTP when generated from HTTPS, so I'm not convinced the fix makes sense.

I thought about this issue, but I couldn't think of how it would be a problem since the secure session cookie is always marked as "secure only." By saving it to the sid column, it *could* be used via HTTP but it won't be, unless someone manually hacked their cookie jar.

mfb’s picture

Status: Needs review » Needs work

OK I missed something serious when thinking about this before, but I since played around with Firecookie on my HTTP+HTTPS sandbox site...

We should ignore the $_COOKIE[$insecure_session_name] on an HTTPS site by default. The reason is that the insecure cookie is, as it says "insecure". And could thus be set (fixated) by a man-in-the-middle on the HTTP site. The contents of this cookie would then be saved into the session table when a user logged in via HTTPS, and then could be used by the man-in-the-middle to gain authenticated access. This is a type of scenario that only allowing logins to the HTTPS site should be able to protect against.

We need to wrap all the insecure_session functionality in checks for the 'https' variable, which ironically gives you access to this less secure functionality.

mfb’s picture

Status: Needs work » Needs review
FileSize
1.93 KB

OK, this is a little better for me. I'm now prevented from attacking my HTTPS login via HTTP man-in-the-middle (unless this 'https' variable is enabled, which hasn't been part of my testing scenario so far).

Damien Tournoud’s picture

We should ignore the $_COOKIE[$insecure_session_name] on an HTTPS site by default. The reason is that the insecure cookie is, as it says "insecure". And could thus be set (fixated) by a man-in-the-middle on the HTTP site. The contents of this cookie would then be saved into the session table when a user logged in via HTTPS, and then could be used by the man-in-the-middle to gain authenticated access. This is a type of scenario that only allowing logins to the HTTPS site should be able to protect against.

Not sure what you mean. We don't do that. When the user logs in on the HTTPS site, his session will get regenerated. We only use the "insecure" cookie to maintain session data from anonymous (browsing the HTTP site) to authenticated (on both the HTTP and HTTPS site).

chx’s picture

Hm, wait. If you are running solely on HTTPS why would the new session code kick in?

mfb’s picture

My comment in #10 and #11 is talking about security in terms of what HTTPS is designed to protect against, e.g. altering contents of HTTP responses, man-in-the-middle attacks etc. The scenario is a site that has both HTTP and HTTPS sites available. The user and user/login pages redirect to HTTPS so all authenticated access is made via HTTPS.

This line of code: $fields['sid'] = $_COOKIE[$insecure_session_name]; runs during an HTTPS request, and allows a cookie that was set on the insecure site to be saved as a valid session identifier. The problem is if a hijacker on the network intercepts a user's HTTP response, and sets their own cookie, and the same user later logs in on the HTTPS site, the hijhacker's cookie would be sent to the HTTPS site and saved into the session table, without being regenerated. It's a type of session fixation attack that I'd like to be able to protect against when I require all authenticated access to use HTTPS.

chx’s picture

When a user logs in on the secure site, the session is created with an empty string sid. <= again wait a second. I think I fought a bit that this can not happen, you never get an empty sid?

mfb’s picture

Are you actually testing on a HTTPS instance? If so it should be quite clear that sid = '', just look in the db after logging in :)

Or just look at the code. in _drupal_session_write(), sid is undefined if $is_https unless isset($_COOKIE[$insecure_session_name]). And default value of sid is '' according to system.install

Status: Needs review » Needs work

The last submitted patch failed testing.

mfb’s picture

Status: Needs work » Needs review

re-test

Status: Needs review » Needs work

The last submitted patch failed testing.

mfb’s picture

Status: Needs work » Needs review
FileSize
1.85 KB

Re-roll.

mfb’s picture

FileSize
5.12 KB

I added tests for HTTPS session handling without the 'https' variable being enabled (i.e. default Drupal configuration).

In this patch I also experimented with some refactoring of _drupal_session_write(): I put both sid and ssid in the db_merge key array to make it more explicit that we are treating both sid and ssid as keys.

Status: Needs review » Needs work

The last submitted patch failed testing.

mfb’s picture

Status: Needs work » Needs review
FileSize
5.7 KB

Oops, forgot that a mock HTTPS request causes a redirect from HTTP to HTTPS. Tweaked session_test_drupal_goto_alter() to ensure that the redirect is altered when HTTPS request has been mocked, which can be determined from a new global variable $is_https_mock

mfb’s picture

By the way, I just noticed that drupal_http_request() timeout option does not work for HTTPS URLs because apparently stream_set_timeout() does not work for SSL connections? See http://www.php.net/manual/en/function.stream-set-timeout.php#93298 Anyone know a work around for this?

This causes the Drupal HTTP request test to fail when running on HTTPS environment.

chx’s picture

Status: Needs review » Needs work

What's not clear from this issue and took a long chat session to clarify: the bug appears if you have a mixed HTTP-HTTPS site and the API is off.
db_merge('sessions')->key($key)->fields($fields)->execute(); needs to multiple lines like the original. $GLOBALS['is_https_mock'] GLOBALS is unnecessary as this code is already in the global scope. Wow, array_fill_keys that's new, first time I see this function, handy.

// Alter the redirect when using a mock HTTPS request. this needs to be // Alter the redirect when using a mock HTTPS request and not when on real HTTPS because...

If you need to skip a test because of a PHP bug when the testing environment is on HTTPS, please do it. If there is no bugs.php.net report then please create one and link to it.

And thanks a ton for the great work on this.

mfb’s picture

Status: Needs work » Needs review
FileSize
8.65 KB

This should resolve everything in #25

dww’s picture

Status: Needs review » Reviewed & tested by the community

I just saw this issue, and I'm happy to report that it doesn't conflict at all with #607008: Fix bugs in https support and force using https for authorize.php if available -- neither in code nor in functionality. Yay. ;) Tested authorize.php in the mixed http/https case with this applied, all is still well. I read over the patch -- great work on the tests! All look sane and reasonable. Patch fixes an important bug, code is clean and happy. RTBC!

webchick’s picture

Status: Reviewed & tested by the community » Needs work
+++ includes/session.inc	28 Oct 2009 19:50:25 -0000
@@ -151,12 +151,18 @@ function _drupal_session_write($sid, $va
+    // If the "secure pages" setting is enabled, use the insecure session
+    // identifier as the sid.

Wait. What? That makes no sense. :) If secure pages is /enabled/, don't I want to use the /secure/ session?

Could you please clarify this comment?

+++ modules/simpletest/tests/session.test	28 Oct 2009 19:50:25 -0000
@@ -272,18 +272,61 @@ class SessionHttpsTestCase extends Drupa
+    $this->drupalGet('user');
+    $form = $this->xpath('//form[@id="user-login"]');
+    $form[0]['action'] = $this->httpsUrl('user');
...
+    $this->drupalGet('user');
+    $form = $this->xpath('//form[@id="user-login"]');
+    $form[0]['action'] = $this->httpsUrl('user');

This code looks really odd. Could we just drupalGet('user/login') instead?

Then just as a general note, there is a ton of really useful discussion here that we should make sure is reflected in either the code comments or the tests. I'm not sure the importance of this fix is properly communicated, yet.

I'm on crack. Are you, too?

mfb’s picture

Wait. What? That makes no sense. :) If secure pages is /enabled/, don't I want to use the /secure/ session?

One would think :( This so-called 'https' or "secure pages" setting is not required for HTTPS support. What it does is force you to use both secure HTTPS and insecure HTTP sessions. *Not* enabling this setting is intrinsically *more* secure because it allows a site to have only secure HTTPS authenticated sessions (which prevents session hijacking by someone able to snarf network traffic, man-in-the-middle attacks, etc.)

In this patch I am really just trying to fix a critical bug. If I took a more holistic approach I'd probably rewrite the HTTPS support to have multiple variables. The 'https' variable triggers multiple bits of logic throughout core, some of which may be desired and some of which may not be desired depending on the business logic for each site. For example I might want to be able to force certain URLs to redirect from HTTP to HTTPS or vice versa, but not force secure authenticated sessions to use both secure and insecure session cookies.

This code looks really odd. Could we just drupalGet('user/login') instead?

I can add more comments in the test. This code is more-or-less copied from the existing HTTPS tests, which create a mock HTTPS form submission by modifying the form action to route through a https.php file.

chx’s picture

if you have a https only site you need no api, it Just Works. Or it worked ;) anyways, it's kinda obvious that this API is for mixed HTTP-HTTPS because a HTTPS site is slow. Very very very slow. Unless you have a hardware SSL accelerator which costs mucho money.

mfb’s picture

Status: Needs work » Needs review
FileSize
9.21 KB

Added some additional comments to address #28. Also, fixed one additional test failure in DrupalHTTPRequestTestCase when the test environment is using HTTPS.

chx’s picture

Status: Needs review » Reviewed & tested by the community

Back to the green queue :)

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Cool, thank you!

Committed to HEAD.

deekayen’s picture

Status: Fixed » Needs work
FileSize
24.72 KB

After this issue was committed, my PIFR 2 client threw 3 exceptions on SessionHttpsTestCase.

http://testing.drupal.org/pifr-2.x/pifr/test/16137

I'll leave the client failure up for a couple hours. Screenshot of specifics attached as well.

mfb’s picture

Status: Needs work » Postponed (maintainer needs more info)

I can't reproduce these notices so hopefully someone else can fix or give me more info. E.g. which calls to drupal_http_request() are throwing the notices? Apparently the response code, status message and data are all missing, i.e. there is no response at all.

deekayen’s picture

Status: Postponed (maintainer needs more info) » Fixed

I'll mark this fixed again. I think I have an idea about what might be going on.

deekayen’s picture

The tests fail if a non-https service is running on port 443, like a tor bridge. I guess that's probably not a worthy reason for re-opening this issue.

deekayen’s picture

None of the automated test clients are setup for testing https. Should they be?

webchick’s picture

@deekayen: That was a spammer re-posting mfb's comment from #5. Blocked now.

deekayen’s picture

I wasn't responding to the spammer. I had a non-https service running on port 443, which the test tried to use and failed, so that suggests to me that there's something that the tests could check on 443 if it was working properly. Is it better that the test bots have SSL available for tests, or better that the tests can verify that SSL doesn't exist, or a combo of both (which would lead to inconsistent failures)?

mfb’s picture

Can you clarify? I don't know why the tests would try to use port 443, unless the test environment itself is on port 443.

webchick’s picture

This discussion is probably better moved to the infrastructure queue or PIFT or wherever people talk about testing bot set-ups. :)

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

mfb’s picture

Status: Closed (fixed) » Active

Uhg, this horrible bug is back, at least in my test site :(

Unfortunately I never got a chance to review #813492: HTTPS sessions use an invalid merge query, which rolled back part of this patch and appears to have re-introduced the bug.

The problem is that we are saving rows to the sessions table with empty string values for sid or ssid. And then we are selecting these rows and finding a valid session when someone passes in a session cookie with an empty string value.

Steps to reproduce:

  1. Install Drupal and make available on HTTP and HTTPS URLs, e.g. http://example.com/ and https://example.com/.
  2. Login to the HTTP site as uid 1 (this results in a session with ssid = '') and the HTTPS site as uid 1 (this results in a session with sid = '').
  3. Bring up the Firecookie tab in Firebug.
  4. Hover over both the SESS and SSESS cookies, right click and select Clear Value.
  5. Now visit the HTTP and HTTPS sites with your empty string session cookies. You will still be uid 1 on both sites. So will anyone else visiting the sites with empty string session cookies!
  6. Profit.
mfb’s picture

BTW, with the empty but valid session ID, I get Notice: A session had already been started - ignoring session_start() in drupal_session_start() (line 272 of session.inc). This is because drupal_session_started() returns $session_started && session_id(), and session_id() is the empty string so it returns FALSE even though the session was in fact started.

mfb’s picture

Status: Active » Needs review
FileSize
2.66 KB

Here's what I have so far. First of all sessions.sid and sessions.ssid should not have a default value. Then we need to set values for both columns in the db_merge().

Also, changed the column descriptions to reflect that session IDs are no longer generated by PHP's session API but by Drupal's session handlers.

This still needs tests.

Status: Needs review » Needs work

The last submitted patch, 575280-empty-session-id.patch, failed testing.

Anonymous’s picture

subscribe. nice catch, looking into it.

mfb’s picture

Here's another patch. Also fix up some db schema updates.

#55 stored the insecure session ID in both sid and ssid columns, but secure session IDs should be secure only. In this version I store an empty string as the secure session ID (otherwise we'd have to generate a secure session ID, which takes time) but also add some logic in _drupal_session_read() to prevent empty strings from actually being selected as valid secure session IDs.

mfb’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 575280-empty-session-id.patch, failed testing.

catch’s picture

The edits to system_update_7057() should be in a new update handler, we need to support sites that have already installed beta1 now.

mfb’s picture

Status: Needs work » Needs review
FileSize
7.51 KB

OK. Also might have found the problem with all the upgrade path errors.

mfb’s picture

This version just removes the last hunk, it shouldn't be needed.

I see now how session tests passed in #813492: HTTPS sessions use an invalid merge query:

-    $this->assertSessionIds($ssid, $ssid, 'Session has two secure SIDs');
+    $this->assertSessionIds('', $ssid, 'Session has NULL for SID and a correct secure SID.');

Modifying the test, pretty sneaky.. ;)

dww’s picture

Do we need a comment at the assertion in the test pointing to this issue nid saying "don't change me"? :/

Glad you tracked this down again, mfb... thanks!

mfb’s picture

Added another test: login to the insecure site, and verify that the resulting empty secure session ID saved to the sessions table cannot be used to access the secure site.

Added a new file for mocking HTTP requests so the test works on both HTTP and HTTPS test environments.

mfb’s picture

Add another test: login to the secure site, and verify that an empty session ID cannot be used to access the non-secure site. Like the above test, this test fails without the patch.

mfb’s picture

Tweak comments and add some missing doxygen

chx’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
9.95 KB

The patch is good but had a chance of a notice in the new http.php. Added a !empty to it.

mfb’s picture

Thanks. and, this just changes a comment from "http" to "HTTP"

grendzy’s picture

Status: Reviewed & tested by the community » Needs review

$conf['https'] should be documented in default.settings.php, yes?

[EDIT] - this was a cross-post, I hadn't intended to change the status. Still, I'm a bit confused by $conf['https'] - that seems like a weird choice considering what it does. It seems more like $conf['enable_https_mixed_sessions'] or something. As it stands we have this:

http://drupal.org/https-information : For better security, leave $conf['https'] at the default value (FALSE) and only use HTTPS for authenticated sessions.

So we're supposed to turn 'https' off to turn it on?

mfb’s picture

Technically speaking, this bug has nothing to do with $conf['https'] -- I have only been testing a default install, without enabling the poorly named $conf['https'] setting.

I totally agree, $conf['enable_https_mixed_sessions'] is a much better name. Maybe $conf['session_mixed_https'] to match the existing $conf['session_write_interval'].

I haven't done much testing of $conf['https'] mainly because I don't really understand the security model. With HTTPS-only authenticated sessions it's pretty straightforward -- attackers shouldn't be able to read or tamper with or replay any authenticated requests or responses; authenticated session keys and all data sent to and from the site should be safe. With mixed HTTPS it's another story.

grendzy’s picture

cool, thanks - I'm working on the port of securepages.module so I'll do some more research later on $conf['https'].

Setting the status back per #68.

grendzy’s picture

Status: Needs review » Reviewed & tested by the community

doh.

mfb’s picture

BTW I made some updates to https://drupal.org/https-information. Would be great if someone could add info on proper setup for $conf['https'] -- Presumably needs contrib modules like Secure Pages to prevent insecure sessions from accessing certain pages, forms, etc.

carlos8f’s picture

Extremely scary security bug! So glad to see we've noticed this regression now and not after 7.0 release :P

bleen’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
10.93 KB

This is the identical patch from #69 with two whitespace issue removed.

Damien Tournoud’s picture

+// Negated copy of the condition in _drupal_bootstrap(). If the user agent is
+// not from simpletest then disallow access.
+if (!(isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE))) {
+  exit;
+}

This (from both http.php and https.php) rely on the fact that it is difficult from the client side to forge the User Agent header. Are we sure that's always the case? For example what about XMLHttpRequest and HTTP requests from Flash content?

carlos8f’s picture

I think http://api.drupal.org/api/function/drupal_valid_test_ua/7 is the proper way to validate the request, unfortunately it requires DRUPAL_ROOT and REQUEST_TIME to be pre-defined. Maybe we can have http.php and https.php have private versions of drupal_valid_test_ua() that use variables instead of constants to generate the HMAC.

chx’s picture

Status: Needs review » Reviewed & tested by the community

It does not rely on that. If you forge your user agent to start with "simpletest" then the check in bootstrap will kick in. That's what we ensure here. The actual UA check is in bootstrap.

carlos8f’s picture

Status: Reviewed & tested by the community » Needs review

By the way, "forging" the User-Agent string is trivial. Anyone can set that to include the word "simpletest". Checking for that word alone (and not comparing the HMAC) in the User-Agent is not really enforcing anything.

carlos8f’s picture

Status: Needs review » Needs work

Cross-posted the status, but it looks like there is a problem with the User-Agent check.

In http.php we are checking if the User-Agent contains the string "simpletest", but in bootstrap.inc we check if the User-Agent starts with "simpletest" and then has digits and a semicolon, and only validates then. If you use "something simpletest" as the User-Agent to http(s).php, it passes the initial check, and the bootstrap.inc check is ignored. That would allow http(s).php to be used to make non-test-db requests and fool the server into thinking it is in one mode or the other.

chx’s picture

Assigned: Unassigned » chx

Working on it.

carlos8f’s picture

Assigned: chx » Unassigned
Status: Needs work » Needs review
FileSize
11.66 KB

Now using the exact same preg_match() as bootstrap.inc, this should prevent any HTTP/S spoofing attacks.

chx’s picture

Assigned: Unassigned » chx
FileSize
12.64 KB

Here we go. This will make sure it can't be broken again.

carlos8f’s picture

Assigned: chx » Unassigned
Status: Needs review » Reviewed & tested by the community

#84 is OK for me. There are several places in core that can be cleaned up by this new function, we can leave that to a follow-up though. install.core.inc uses a similar strpos() on the User-Agent, but in that instance it is pointless since we call drupal_boostrap() directly after.

Status: Reviewed & tested by the community » Needs work
Issue tags: -Release blocker, -Security

The last submitted patch, 575280-empty-session-id.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
Issue tags: +Release blocker, +Security

#84: 575280-empty-session-id.patch queued for re-testing.

carlos8f’s picture

Status: Needs review » Reviewed & tested by the community

good

webchick’s picture

Status: Reviewed & tested by the community » Needs work

Summary of IRC discussion:

+ * This function does not test for forged headers, it only check whether the
+ * user agent looks like simpletest. drupal_valid_test_ua() needs to be fired
+ * to verify the validity of the UA. This function is called from various
+ * simpletest helpers.

I don't understand the point of this, then. Having two functions that are kinda-sorta validating the HTTP user agent strikes me as practically begging for further security problems. Let's move that logic into drupal_valid_test_ua() and have drupal_valid_test_ua() return the SimpleTest prefix.

chx’s picture

Status: Needs work » Needs review
FileSize
15.21 KB

Sure thing, I can do that. I have found the weirdest bug ever: drupal_get_bootstrap_phase messes up bootstrap if called before there was a DRUPAL_BOOTSTRAP_FULL phase which we have not found 'cos the only way to get there is by including bootstrap.inc from somethign that's not index.php. But this patch enhances the ability of Drupal to work with other systems.

carlos8f’s picture

Status: Needs review » Reviewed & tested by the community

I looked at the patch for a while comparing the changes vs. #76 and it seems correct. At the same time it improves the worth of drupal_valid_test_ua(), too. Very nice.

Status: Reviewed & tested by the community » Needs work
Issue tags: -Release blocker, -Security

The last submitted patch, 575280-empty-session-id.patch, failed testing.

aspilicious’s picture

Status: Needs work » Needs review
Issue tags: +Release blocker, +Security

#90: 575280-empty-session-id.patch queued for re-testing.

mfb’s picture

Hrm there's nothing preventing Drupal from running the full page load thru these URLs, just use a Firefox plugin to change your user agent to "simpletest123;" and you can load https://localhost/d7/modules/simpletest/tests/http.php?q=user or http://localhost/d7/modules/simpletest/tests/https.php?q=user (when will Drupal will move itself out of the document root...)

carlos8f’s picture

@mfb actually you're right. We removed the 403 exit in #90, so we still have the same security issue with HTTP/S spoofing. I still think #83 is an easy way to fix the problem, just use consistent preg_match() in http.php, the hmac will get checked in bootstrap.inc and we'd be denied access. #90 would just treat it as a normal request, which would allow the spoof.

chx’s picture

That's not a spoofing issue however. The invalid UA is disregarded. The fact that it loads proves this -- if it would use the simpletest prefix it would not find the tables.

mfb’s picture

Well IMO we should figure out some way to ensure that only the sites' own tests can load http.php and https.php. and this should happen before database initialization, in case there is some pre-database page cache delivering pages. Otherwise it sure feels like a backdoor to the site (granted I haven't thought of a specific exploit but it's Sunday and I'm supposed to be doing other things now :)

carlos8f’s picture

In HEAD we check for a possible spoof in _drupal_bootstrap_database(), for our purposes here that's good enough.

#90 has the same flaw as #76 it seems, just using a different spoof header ("simpletest123;" instead of "simpletest blah"). I'm re-uploading #83, and here's the difference between #76 and this:

interdiff 575280-empty-session-id_8.patch 575280-empty-session-id_9.patch 
diff -u modules/simpletest/tests/http.php modules/simpletest/tests/http.php
--- modules/simpletest/tests/http.php	29 Oct 2010 22:10:36 -0000
+++ modules/simpletest/tests/http.php	31 Oct 2010 19:09:10 -0000
@@ -8,7 +8,7 @@
 
 // Negated copy of the condition in _drupal_bootstrap(). If the user agent is
 // not from simpletest then disallow access.
-if (!(isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE))) {
+if (!(isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);/", $_SERVER['HTTP_USER_AGENT']))) {
   exit;
 }

The change means you can't spoof using "foo simpletest". If you try "simpletest123;" and get through this check, the hmac is checked in bootstrap.inc and the request dies with access denied. I think that is sufficient to address this side issue and get the larger picture (critical sid vulnerability) fixed.

mfb’s picture

OK I am now getting 403. A real edge case, but I believe page cache could still be accessible, e.g. for $conf['page_cache_without_database'] then the page cache could deliver what it thinks is a HTTPS request via HTTP before bootstrapping the db.

carlos8f’s picture

Yeah, no worry for cached responses; checking in the bootstrap database phase is fine. If the response is cached then a spoof would be unable to accomplish anything, in theory.

mfb’s picture

Yes, the only thing that could be accomplished would be loading a cached page. A request for http://localhost/d7/modules/simpletest/tests/https.php?q=node will trigger a cache_get for the page cache of https://localhost/d7/index.php?q=node This could be a work-around for any logic that certain pages only be delivered via HTTPS. Let's say you use SSL client authentication to control access on corporate intranet, or you are serving content to users in a country where the content is illegal, but part of the site is also available via HTTP. Like I said a real edge case, does anyone use SSL to verify the client on part of their site.. But seemed worth bringing up.

chx’s picture

Well the solution then is to call drupal_valid_test_ua() twice once immediately after configuration is loaded second when the prefix is needed. Can you guys beat that? I have added static caching to the function so it's very fast.

Edit: it is possible that this check completely obsoletes the one in http.php and https.php which would be quite great.

carlos8f’s picture

I don't know, I still think all we need is to change 2 lines to fix the problem (#98). Anything more than that is catering too much to this edge-case (alternative entry point which modifies server variables, a rare occurrence).

#102 looks like voodoo, calling drupal_valid_test_ua() at the end of bootstrap configuration, the function returns a simpletest prefix now (which is not documented) and that is not used until later... where it is pulled from the static... and $http_deny = TRUE by default, that is not obvious either... plus the static is broken for normal page loads: static is only set if the preg_match() passes. I am generally unhappy with this patch. It overly complicated and caters to edge-case.

chx’s picture

My poor patch throws a notice but it's SO early that noone evah finds it. Also the check is now completely unneeded in http.php, https.php and install.core.inc because it's all neatly wrapped in the function. That actually simplifies and hardens Drupal.

chx’s picture

So to recap, the second security hole we discovered and fixed in the recent patches:

  1. Set $conf['page_cache_without_database'] to TRUE with additional config / modules to make it work.
  2. Enable page caching
  3. Change your site to serve different pages on SSL than without (say, without certain pages are not served at all)
  4. Restrict access to anonymous SSL by firewall or client certificates
  5. the attacker sets user agent to simpletest1 and visits the https.php to request the pages set up and secured in the last two steps. They will be served from cache.

The first sechole, just to make this post a complete summary (copied from mfb):

  1. Install Drupal and make available on HTTP and HTTPS URLs, e.g. http://example.com/ and https://example.com/.
  2. Login to the HTTP site as uid 1 (this results in a session with ssid = '') and the HTTPS site as uid 1 (this results in a session with sid = '').
  3. Bring up the Firecookie tab in Firebug.
  4. Hover over both the SESS and SSESS cookies, right click and select Clear Value.
  5. Now visit the HTTP and HTTPS sites with your empty string session cookies. You will still be uid 1 on both sites. So will anyone else visiting the sites with empty string session cookies!
  6. Profit.
carlos8f’s picture

Status: Needs review » Needs work
+++ includes/bootstrap.inc	2010-11-01 02:28:15 +0000
@@ -2217,26 +2211,53 @@ function _drupal_bootstrap_page_header()
+ * @param $deny
+ *   Whether to issue HTTP deny headers and exit if the user agent is a forged
+ *   simpletest string.
...
-function drupal_valid_test_ua($user_agent) {
+function drupal_valid_test_ua($user_agent = '', $http_deny = TRUE) {

Docs say $deny, but function sig says $http_deny.

+++ includes/session.inc	2010-10-31 18:42:05 +0000
@@ -180,21 +183,23 @@ function _drupal_session_write($sid, $va
+      // Use the session ID as 'sid' and an empty string as 'ssid', which
+      // _drupal_session_read() will prevent from being selected as a valid
+      // secure session ID.

I don't understand what this is saying. Maybe it can be written more clearly.

Other than those two things, this patch looks OK. It moves the spoof check earlier into the boot configuration phase, which prevents "attackers" from peeking at cached pages that they might not normally be able to see through HTTP. This is a pre-existing, totally unrelated issue to the sid flaw and beyond the scope of this issue IMO, but since we have a working patch we might as well go with it. Fix the docs and I think we are done here.

chx’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
16.1 KB

Fixed the docs as asked. Given that the session fix is good since #69 now carlos8f said we are good to go, I set to RTBC.

Status: Reviewed & tested by the community » Needs work
Issue tags: -Release blocker, -Security

The last submitted patch, 575280-empty-session-id.patch, failed testing.

Pls’s picture

Status: Needs work » Needs review

#107: 575280-empty-session-id.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +Release blocker, +Security

The last submitted patch, 575280-empty-session-id.patch, failed testing.

carlos8f’s picture

+++ includes/install.core.inc	2010-11-01 02:28:49 +0000
@@ -228,14 +228,6 @@ function install_begin_request(&$install
 
-  // The user agent header is used to pass a database prefix in the request when
-  // running tests. However, for security reasons, it is imperative that no
-  // installation be permitted using such a prefix.
-  if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) {
-    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
-    exit;
-  }
-
   drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);

Latest patch fails because chx tried to sneak this in. We had been talking about it on IRC and decided it was a useless check. This prevents simpletest from ever using install.php. However, it's next to impossible to forge a legitimate simpletest header, and as long as we check the HMAC (which booting configuration now does) I think we can remove the lines in simpletest.test that checks for a 403.

Powered by Dreditor.

mfb’s picture

or, if you want to leave the test in, change it to use an invalid test UA so bootstrap configuration returns the 403:
$this->drupalGet($base_url . '/install.php', array('external' => TRUE), array('User-Agent', 'simpletest123;'));

carlos8f’s picture

@mfb, I think that works (overridding CURLOPT_USERAGENT with CURLOPT_HTTPHEADER), but the header should be one string, like User-Agent: simpletest123;.

chx’s picture

I would think that you want to use additionalCurlOptions. I did. The patch has failures yet I post it 'cos I am out of time to work on this now.

carlos8f’s picture

+      $this->additionalCurlOptions = array(CURLOPT_USERAGENT => $this->databasePrefix);
$this->assertResponse(403, 'Cannot access install.php with a "simpletest" user-agent header.');

This assertion is not what mfb suggested. We want to make sure install.php returns a 403 if a spoofed simpletest header is present, i.e. "simpletest123;". $this->databasePrefix would produce a valid header and a corresponding 200 OK.

mfb’s picture

also that would set curl options after the request we want to tweak (which then affect later requests). this should get tests passing: $this->drupalGet($base_url . '/install.php', array('external' => TRUE), array('User-Agent: simpletest123;'));

chx’s picture

$this->databasePrefix definitely would not produce a legit header because it lacks timestamp and HMAC. And passing in User-Agent: leads to a conflicting situation (a preset cURL option vs this HTTP header), the variable I used is designed to override any CURL options set.

carlos8f’s picture

OK, in any case we need to make the assertion more clear. It says "Cannot access install.php with a "simpletest" user-agent header." Let's change that to "invalid simpletest user-agent header", and perhaps use simpletest123; instead of $this->databasePrefix. I tested out passing the User-Agent string directly in the drupalGet(), it should work to override CURLOPT_USERAGENT temporarily.

Additionally, I think we should add assertions to test for a 403 when trying to use a spoofed simpletest header on index.php, http.php, and https.php. Then we could group all these 403 assertions into a new test method, and use $this->additionalCurlOptions.

carlos8f’s picture

Assigned: Unassigned » carlos8f

I'm working on cleaning this up a bit.

KeithH’s picture

As far as I'm concerned, once the overlay bug and Dashboard module bug are closed along with this, that then should mean that Drupal seven will be ready for an RC release.
If I can help by testing patches as long as your patches and such have to deal with using a screen reading program or keyboared alone in Drupal7-I can test them out and let you folks know.
Overlay patch loks fine as far as I'm concerned.
I can't wait until all the issues reach zero and an RC build is released.
I think judging on the number of various bugs I've seen is that the more time spent on these the longer Drupal Seven will not be released. I'd like to see this happen ASAP.
Although I don't have the resources at the moment to fix any outstanding issues myself, I can apply various patches and such.
But like I said-if the patch isn't a screen reader specific type of patch as that would have to deal with using the keyboard and such, then I can't really test for anything.
Let's get an RC out soon, hopefully!
I believe that modules and bugs can always be easily addresed during/after an RC, and even after the oficial release.
Please see comment 182 (I think) on the overlay module screen reader chriticle issue and you'll see my opinion on what we all need to do together as a community to get Drupal into an RC release, and finally the next stable. :)
Let's make Drupal the best we can make it be for all users!

carlos8f’s picture

Status: Needs work » Needs review
FileSize
19.57 KB

It turns out we can't remove the user agent check in http(s).php, like #104 states. That's because if you have a user agent that looks nothing like simpletest, it is ignored as a simpletest spoof and you can happily make requests to http(s).php using your browser (I was able to verify that).

This patch has a slightly different implementation than @chx's, but I tried to keep the goal the same. It locks down http(s).php so you can never make fake requests to it. To do that, I re-arranged some things in http(s).php and have developed a sneaky way to extract the hash salt from settings.php when we're running the tests.

As a side, a 403 is no longer returned for requests to index.php when the user agent looks like 'simpletest123', it's simply treated as a normal request to index.php.

Let's see how the tests do.

Status: Needs review » Needs work

The last submitted patch, 575280-empty-session-id_16.patch, failed testing.

carlos8f’s picture

Status: Needs work » Needs review
FileSize
19.75 KB

I learned why if (!$this->inCURL()) is necessary in simpletest.test.

carlos8f’s picture

Fixed a problem with the static in drupal_valid_test_ua().

carlos8f’s picture

Even better, instead of "forging" the header with "simpletest123;", let's use an actual header and slightly modify the HMAC. That ensures the HMAC is actually getting validated.

carlos8f’s picture

Improved the test again, now we check for a 200 with our generated $test_ua, then change the HMAC and test for 403.

mfb’s picture

I don't think you can/should try to parse PHP with preg_match.. what about people modifying the formatting, commenting it out, etc? How about bootstrap configuration so salt is available, validate the test UA and then bootstrap full.

BTW, chx said something about finding a bootstrap bug, not sure if fix for that is included in this.

carlos8f’s picture

Status: Needs review » Needs work

Yes, that sounds like a better idea. I will include @chx's bootstrap bugfix in there too, although I'm not sure if it affects my patch.

carlos8f’s picture

Status: Needs work » Needs review
FileSize
20.38 KB

Much better.

carlos8f’s picture

On second thought, the bootstrap bugfix doesn't affect the patch, so I left it out and let's open a separate issue if necessary.

mfb’s picture

Hmm looks like you changed my patch to bootstrap config and then do the mock HTTPS/HTTP thing. I would have assumed that doesn't work since some drupal variables wouldn't be initialized as expected.

carlos8f’s picture

Doh. I think that it works because we override $_SERVER['HTTPS'] etc. afterwards anyway, but still, we need to simulate that PHP started out with these server variables to begin with.

carlos8f’s picture

@mfb, sorry, for some reason I totally missed that you posted a patch in #127. Looks like #132 is basically the same though, other than the capitalization of 'HTTPS' in comments.

chx’s picture

Status: Needs review » Needs work

No, no, no! We are not exposing the salt like this. Working on it. Nevermind: i am stupid.

chx’s picture

Status: Needs work » Reviewed & tested by the community

Sorry for the noise. mfb's patch was good http://drupal.org/node/575280#comment-3663234 here linking the patch http://drupal.org/files/issues/575280-empty-session-id_21.patch itself and carlos8f said that he missed the patch and basically implemented the safe and interdiff concurs. So while the latest two green patches are largely the same the comments in mfb's is better. Let's get this sucker in!

Dries’s picture

Status: Reviewed & tested by the community » Fixed

Reviewed and studdied, and committed to CVS HEAD. Thanks.

lafitz’s picture

Status: Fixed » Needs review

session.patch queued for re-testing.

carlos8f’s picture

Status: Needs review » Fixed

This issue is already fixed.

mfb’s picture

Status: Fixed » Needs review
FileSize
663 bytes

or is it. I might have screwed up the update function -- don't we need to recreate the primary key for postgres? It would be bad if postgres was left hanging without a primary key here!

carlos8f’s picture

I've never used Postgres.... why does the primary key need to be dropped and re-added?

Also... is it possible that after this update runs, there are still rows in {sessions} where sid = '', ssid = '...' that still pose a security problem if someone uses a blank sid in their cookie? We do if ($sid) { in $is_https mode, but not otherwise. Seems like we should always set $user to drupal_anonymous_user() if we have a blank $sid.

Another minor thing, in _drupal_session_read() it's possible if $is_https and !$sid, $user is undefined and might trigger a PHP notice.

grendzy’s picture

Status: Needs review » Needs work

The last commit seems to create duplicate entries in the session table when $conf['https'] = TRUE.

Steps to reproduce:
- install HEAD, set $conf['https'] = TRUE.
- Log in via HTTPS
- post a comment via HTTPS.
- post another comment via HTTP.

The session table now looks like this:

+-----+---------------------------------------------+---------------------------------------------+
| uid | sid                                         | ssid                                        |
+-----+---------------------------------------------+---------------------------------------------+
|   1 | 1VnxAJgfdR1e49PrjP1WFXmbJldcyFVm18ZXzgOvbNc |                                             | 
|   1 | 1VnxAJgfdR1e49PrjP1WFXmbJldcyFVm18ZXzgOvbNc | 80wsjXv_o5OMFZBxFQQtbDZmNbE9SpsRhBhCWDYycgs | 
+-----+---------------------------------------------+---------------------------------------------+

As a result, you get really strange behavior, such as drupal_get_messages() failing to retrieve messages that are stuck in the second row.

carlos8f’s picture

Assigned: carlos8f » Unassigned

.

mfb’s picture

Assigned: Unassigned » carlos8f
Status: Needs work » Needs review
FileSize
1.29 KB

Does this patch fix the issue in #142?
Re: #141 see the docs at http://api.drupal.org/api/drupal/includes--database--database.inc/functi...
The global $user declaration in _drupal_session_read() sets $user to NULL if it doesn't exist so there shouldn't be any chance of a notice.

EvanDonovan’s picture

Assigned: carlos8f » Unassigned

Cross-post.

grendzy’s picture

Yes, thanks! The latest patch fixes the issue I was seeing with duplicate sessions. I've noticed 2 more issues with https, though it's unclear if they are related to this patch:

If an anonymous session is being written because of drupal_set_message(), and it's an https request, $_COOKIE is empty and this test fails:
if (isset($_COOKIE[$insecure_session_name])) {
As a result the insecure cookie never gets sent, and we end up with the following session:

+-----+---------------------------------------------+---------------------------------------------+
| uid | sid                                         | ssid                                        |
+-----+---------------------------------------------+---------------------------------------------+
|   0 | A1ueD72U6wncR4EQMU3g8aR3ptRCcUtjcHNiK7FR4lI | A1ueD72U6wncR4EQMU3g8aR3ptRCcUtjcHNiK7FR4lI | 
+-----+---------------------------------------------+---------------------------------------------+

Second issue, $user->timestamp appears not to be defined for the anonymous user; leading to PHP notices in some of my tests.
if ($is_changed || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180))

If a clearer demonstration of what I'm seeing would be helpful, just checkout the 7.x branch of securepages and run the tests (you'll also need the patch in #961508: Dual http/https session cookies interfere with drupal_valid_token()).

carlos8f’s picture

FileSize
2.7 KB

We still haven't fixed the real problem: as Damien points out we are accepting an empty session ID. We've fixed this for $is_https, but not otherwise.

This is a reroll of #144 which returns an anonymous user if empty($sid). @mfb's changes to the update function kill sessions with blank IDs, but sites may have already run that update function and I think we should fix it on the session handler level as well.

chx’s picture

Status: Needs review » Reviewed & tested by the community

Well, this is good.

webchick’s picture

Issue tags: +Needs tests

We need tests for this.

webchick’s picture

Status: Reviewed & tested by the community » Needs work
carlos8f’s picture

Assigned: Unassigned » carlos8f

okay.

webchick’s picture

Come on IRC! :D

carlos8f’s picture

Status: Needs work » Needs review
FileSize
6.93 KB
3.83 KB

The bot should fail the first patch, since it does not include the fix. I moved the fix to drupal_session_initialize(), and simply check !empty($_COOKIE[$session_name]) instead of isset(), so now we don't even session_start() if a blank session ID is passed.

This does not include a test for #142, so it still needs work.

Status: Needs review » Needs work

The last submitted patch, 575280-empty-session-id-test.patch, failed testing.

carlos8f’s picture

Status: Needs work » Needs review
FileSize
6.54 KB

OK, here is a straight isset() to !empty() change, let's see how the bot likes this one.

chx’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
5.63 KB

While I was on the fence with the previous patch (because it changed session writes) I really like this one. I have deleted one assert from the test however -- there is no need to assert the success of a database update. We have update tests for that.

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed to HEAD! Thanks!

Status: Fixed » Closed (fixed)
Issue tags: -Needs tests, -Release blocker, -Security

Automatically closed -- issue fixed for 2 weeks with no activity.