We believe that commit 03d1a3e: Issue #1962458 by jeremyr - 403 for anonymous users on node/add causes redirects from HTTPS in the admin section and other important URLs (like node/*edit) due to an interaction with Session443, or a bug in the implementation of this commit.


Background:

Two days ago I upgraded from Barracuda BOA-2.0.8 to Barracuda BOA-2.0.9, and logged the output of this upgrade, this server is running Drupal 6 sites, the main one being https://www.transitionnetwork.org/

The site has the Session 443 plugin installed to ensure that all authenticated sessions use HTTPS.

Since the upgrade to BOA-2.0.9 all HTTPS requests to /admin* and /node/*/edit (amongst others) that use clean URLs get a 301 redirect to the front page of the site - see an example of this.

No other configuration changes were made to the server prior/since the BOA upgrade.

There are two work-arounds for this problem, if you have Request Policy for Firefox installed then that can be used to block all HTTPS to HTTP redirects, or you can use non-clean URLs.

The issue was tracked down to an SSL-related problem as the problem mostly disappeared when that module was disabled - but session handling across the HTTP(S) contexts was not quite right.

Reverting said commit fixed the problem.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Jim Kirkpatrick’s picture

To add to Chris' description:

It seems that only /admin* and /node/*/edit type pages are redirected -- i.e. those with forms or in the admin. User edit pages (user/*/edit) are fine - this affects content only.

The workaround is to use the non-clean url format, e.g. https://www.transitionnetwork.org/?q=admin.

Please note we also have our override.global.inc file in place that sets up some environment variables and also includes the code recommended by the Session 443 module - this could potentially be redundant or an issue. It looks like:

if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') {
  ini_set('session.cookie_secure', 1);
}

EDIT: We tried commenting the session secure cookie line out and that made no difference - we didn't expect it to but it's the only customisation away from BOA defaults we have, and it's now ruled out.

chrisc’s picture

Jim Kirkpatrick’s picture

Title: BOA-2.0.9 Redirects HTTPS requests to HTTP » BOA-2.0.9 Redirects HTTPS requests to HTTP since "403 for anonymous" feature.

More information: It seems that reverting commit 03d1a3e: Issue #1962458 by jeremyr - 403 for anonymous users on node/add has fixed it.

Also, disabling Session443 seemed to resolve the issue partially.

I'll update the description.

Jim Kirkpatrick’s picture

Issue summary: View changes

typo fixed

Jim Kirkpatrick’s picture

Component: Nginx Configuration » Code

Confirmed: Reverting the commit where it changed /data/conf/global.inc fixes the issue for us.

The original cause of the commit was this issue: #1962458: 403 for anonymous users on node/add

My ramblings during debugging are available from after this comment over on our Trac.

omega8cc’s picture

Status: Active » Fixed

Fixed in commit: http://drupalcode.org/project/octopus.git/commit/f321101

There were two issues with the code intended to protect never cached URIs from DoS attempts as early in the bootstrap as possible (it couldn't work on the Nginx level, because there are no exceptions/control files available to use):

1. The code didn't respect request scheme.
2. The code forced $_SERVER['SERVER_NAME'] in the redirect instead of $_SERVER['HTTP_HOST']

Now you can easily disable this protection with two control files: one introduced in BOA-2.0.9 allow_anon_node_add.info and new introduced in the commit linked above disable_admin_dos_protection.info

With protection enabled (default) there is still a potential issue with cookie check, because we are using Drupal native way to generate cookie name on the fly for comparison to make sure that the user is logged in the visited site, so if you are using code or module which alters Drupal defaults for cookie naming and/or handling, it may not work and the system may decide that you are an anonymous visitor and redirect you to the homepage:

  if (file_exists('sites/'. $_SERVER['SERVER_NAME'] .'/modules/cookie_domain.info')) {
    $domain = '.'. preg_replace('`^www.`', '', $_SERVER['SERVER_NAME']);
  }
  else {
    $domain = '.'. preg_replace('`^www.`', '', $_SERVER['HTTP_HOST']);
  }
  $domain = str_replace('..', '.', $domain);
  if (count(explode('.', $domain)) > 2) {
    @ini_set('session.cookie_domain', $domain);
    $cookie_domain = $domain;
    header("X-Cookie-Domain: " . $cookie_domain);
  }
    if ($drupal_seven || $drupal_eight) {
      $conf['https'] = TRUE;
      $sess_prefix = ini_get('session.cookie_secure') ? 'SSESS' : 'SESS';
      $test_sess_name = $sess_prefix . substr(hash('sha256', $cookie_domain), 0, 32);
      unset($conf['file_downloads']); // disable hardcoded public downloads in d7
    }
    else {
      $test_sess_name = 'SESS'. md5($cookie_domain);
    }
    if (!isset($_COOKIE[$test_sess_name])) {
      if (preg_match("/\/(?:node\/[0-9]+\/edit|node\/add)/", $_SERVER['REQUEST_URI'])) {
        if (!file_exists('sites/'. $_SERVER['SERVER_NAME'] .'/modules/allow_anon_node_add.info')) {
          $deny_anon = TRUE;
          header("HTTP/1.1 301 Moved Permanently");
          header("Location: " . $base_url . "/");
        }
      }
      if (preg_match("/^\/(?:[a-z]{2}\/)?(?:admin|logout|privatemsg|approve)/", $_SERVER['REQUEST_URI'])) {
        if (!file_exists('sites/'. $_SERVER['SERVER_NAME'] .'/modules/disable_admin_dos_protection.info')) {
          $deny_anon = TRUE;
          header("HTTP/1.1 301 Moved Permanently");
          header("Location: " . $base_url . "/");
        }
      }
    }

I would recommend to not use modules altering default behaviour. After all, our D7 core is patched to support HTTP/HTTPS out of the box, so you could add your own redirects on the /data/conf/override.global.inc level or /data/conf/settings.global.inc for higher level overrides using examples provided in the override.global.inc template.

omega8cc’s picture

Status: Fixed » Needs work

However, I just realized that our code based on D7 core doesn't really work as expected, because we have this check in global.inc

$sess_prefix = ini_get('session.cookie_secure') ? 'SSESS' : 'SESS';

While session.cookie_secure is not set at this stage yet, because it is set in the boostrap.inc *after* the settings.php (and thus global.inc) is included, oops.

omega8cc’s picture

Status: Needs work » Needs review

This should fix the problem: http://drupalcode.org/project/octopus.git/commit/8c79f5e

If you could test this and confirm, that would be great.

Jim Kirkpatrick’s picture

Great, I will test tonight and report back, thank you.

Can you also please clarify: Do we need Session443 under BOA on Drupal 6, or is there a better way to have secure/https forms/admin and sessions?

chrisc’s picture

Regarding, the advice from omega8cc:

I would recommend to not use modules altering default behaviour.

We would be happy not to use the Session 443 module for Drupal 6 if you can suggest a better alternative to achieve what it does:

  • Sets the secure flag on the authenticated session cookie.
  • Redirects authenticated users from HTTP to HTTPS.

We have Session 443 set to:

  • Redirect authenticated users to HTTPS and redirect anonymous users on login/registration pages to HTTPS. Anonymous users visiting other pages may use HTTP or HTTPS.
  • Force all pages with the login block to use HTTPS.

What we want to achieve is:

  • Passwords only sent over encrypted sessions.
  • Session cookies to be secure to ensure they are only set on encrypted sessions.
  • Authenticated users who follow a link to a HTTP page get redirected to HTTPS to avoid the confusion they experiences as they don't understand why they appear to have been logged out.
  • Non-authenticated users allowed to use HTTP and/or HTTPS as they wish.

We are aware that with Drupal 7 there are separate HTTP and HTTPS cookies but the site isn't going to be upgraded to Drupal 7 in the near future.

omega8cc’s picture

I don't have any recommendation for D6 other than upgrade to D7, but if you could test the change introduced in head and provide some feedback in the context of module used, that would be great.

Jim Kirkpatrick’s picture

Hi, sorry about the delay in testing... I replaced our global.inc file with the one from the commit you did above and the same thing happens, except the redirect is to the HTTPS homepage (not HTTP), which is a minor improvement.

I'll put a bit more context as requested... First, the headers from a logged in user clicking going to admin/content/node (some values partly hidden just in case):

Request URL:https://www.transitionnetwork.org/admin/content/node
Request Method:GET
Status Code:301 Moved Permanently

--- Request Headers ---
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-GB,en-US;q=0.8,en;q=0.6
Connection:keep-alive
Cookie:_pk_ref.1.2dc7=%5B%22%22%2C%22%22%2C1369079418%2C%22http%3A%2F%2Fwww.i-jk.co.uk%2Fdrupal-projects%2Ftransition-network-project-sharing-engine%22%5D; SESS9c2fa34093f48e760e86f541579*****=nr5pd1lat4rjfho4633jh*****; LOGGED_IN=1; has_js=1; _pk_id.1.2dc7=bc9d2f4962a42b34.1361448968.60.1369079754.1369075217.; _pk_ses.1.2dc7=*
Host:www.transitionnetwork.org
Referer:https://www.transitionnetwork.org/
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.63 Safari/537.31

--- Response Headers ---
Cache-Control:no-cache, must-revalidate, post-check=0, pre-check=0
Connection:keep-alive
Content-Type:text/html; charset=utf-8
Date:Mon, 20 May 2013 19:56:22 GMT
ETag:"1369079776"
Last-Modified:Mon, 20 May 2013 19:56:16 +0000
Location:https://www.transitionnetwork.org/
Server:nginx
Transfer-Encoding:chunked
Vary:Accept-Encoding
X-Accel-Expires:
X-Allow-Redis:YES
X-Backend:C
X-Cookie-Domain:.transitionnetwork.org
X-Cookie-Sec:YES
X-Device:normal
X-Engine:Octopus 1.0 ET
X-Local-Proto:https
X-NoCache:Skip
X-Purge-Level:6
X-Redis-Prefix:www.transitionnetwork.org_
X-Server-Name:www.transitionnetwork.org
X-Speed-Cache:BYPASS
X-Speed-Cache-Key:/admin/content/node
X-Speed-Cache-UID:nr5pd1lat4rjfho4633jhtm3q2;
X-This-Proto:https

Attached the list of enabled modules we have enabled on the site in case anything jumps out. I wanted to say we're happy with Session443 and just wanted to confirm it's a good choice under D6/BOA, which you now pretty much have. It's set up per Chris' comment above, and we use it instead of Secure Pages because under D6 that module has been dreadful for us.

So I'd say the key area with false positives is around line 393 in global.inc

  if (!isset($_COOKIE[$test_sess_name])) {
      if (preg_match("/\/(?:node\/[0-9]+\/edit|node\/add)/", $_SERVER['REQUEST_URI'])) {
        if (!file_exists('sites/'. $_SERVER['SERVER_NAME'] .'/modules/allow_anon_node_add.info')) {
          $deny_anon = TRUE;
          header("HTTP/1.1 301 Moved Permanently");
          header("Location: " . $base_url . "/");
        }
      }
      if (preg_match("/^\/(?:[a-z]{2}\/)?(?:admin|logout|privatemsg|approve)/", $_SERVER['REQUEST_URI'])) {
        if (!file_exists('sites/'. $_SERVER['SERVER_NAME'] .'/modules/disable_admin_dos_protection.info')) {
          $deny_anon = TRUE;
          header("HTTP/1.1 301 Moved Permanently");
          header("Location: " . $base_url . "/");
        }
      }
    }

So the $_COOKIE[$test_sess_name] must not be set for some reason. I'll try to get the values of $test_sess_name from BOA 2.0.8 and HEAD to compare in the next comment. Oh, and don't have either control .info file on our /modules directory.

Jim Kirkpatrick’s picture

Having looked I see that the session ID inside the $_COOKIE array has a completely different hash to the one being held in $test_sess_name.

That means our value for $test_sess_name = 'SESS'. md5($cookie_domain); (line 372 is not what is actually used by the site. FYI, our $cookie_domain is ".transitionnetwork.org".

So the likely reason moves up to around line 344 where $domain and $cookie_domain are set:

if (file_exists('sites/'. $_SERVER['SERVER_NAME'] .'/modules/cookie_domain.info')) {
     $domain = '.'. preg_replace('`^www.`', '', $_SERVER['SERVER_NAME']);
   }
   else {
     $domain = '.'. preg_replace('`^www.`', '', $_SERVER['HTTP_HOST']);
   }
   $domain = str_replace('..', '.', $domain);
   if (count(explode('.', $domain)) > 2 && !is_numeric(str_replace('.', '', $domain))) {
     @ini_set('session.cookie_domain', $domain);
     $cookie_domain = $domain;
     header("X-Cookie-Domain: " . $cookie_domain);
   }

So... Should we set the sites/'. $_SERVER['SERVER_NAME'] .'/modules/cookie_domain.info' control file? What is that supposed to do? I'll test now...

Jim Kirkpatrick’s picture

That control file made no difference. Tried the HEAD global.inc (with Redis password change), no difference either.

FYI the global.inc we have in place that works for us is basically the stock 2.0.9 one with the if statement from #11 commented out.

I'm done for tonight, but it seems in summary that the wrong session ID is being looked for by $test_sess_name, because the wrong $domain or $cookie_domain is being used to find it in $_COOKIE, meaning the redirects near the $deny_anon = TRUE sections are fired erroneously.

omega8cc’s picture

Status: Needs review » Active

Thank you for the feedback.

The cookie_domain.info control file allows to force SERVER_NAME for cookies, so you will be logged in in all sub-sites managed with Domain Access, if needed/expected. This feature is unrelated here.

As you can see in #11 above the $cookie_domain is correct:

X-Cookie-Domain:.transitionnetwork.org

This means that also $test_sess_name = 'SESS'. md5($cookie_domain); should provide correct value to check against, exactly as this example (we have added an extra header to be able to compare the values generated):

https://demo.aegir.cc/

GET / HTTP/1.1
Host: demo.aegir.cc
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.7,ja;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Referer: https://demo.aegir.cc/hosting/sites
Cookie: SESSda92b3e6957411ba1437786d56ed4e51=44dbdb72cf11cd62ef0aca4bd707c1d5; has_js=1
Connection: keep-alive

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 20 May 2013 22:17:22 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding, Accept-Encoding
X-Accel-Expires: 1
X-Backend: C
X-Allow-Redis: YES
X-Purge-Level: 6
X-Local-Proto: https
X-Cookie-Domain: .demo.aegir.cc
X-Redis-Prefix: o1.v189q.nyc.host8.biz_
X-Sess-Name: SESSda92b3e6957411ba1437786d56ed4e51
Last-Modified: Mon, 20 May 2013 22:17:22 +0000
Cache-Control: no-cache, must-revalidate, post-check=0, pre-check=0
X-Engine: Octopus 1.0 ET
X-Device: normal
X-Speed-Cache: MISS
X-Speed-Cache-UID: 44dbdb72cf11cd62ef0aca4bd707c1d5;
X-Speed-Cache-Key: /
X-NoCache: DrupalCookie
X-GeoIP-Country-Code: US
X-GeoIP-Country-Name: United States
X-This-Proto: https
X-Server-Name: o1.v189q.nyc.host8.biz
Content-Encoding: gzip

As you can see, generated for comparison X-Sess-Name is the same as Drupal own Cookie name. The hash must match, unless this module generates it in some other way. I didn't check the code of the module, but I suspect it generates a cookie which is non-standard (probably because it marks it as secure etc).

Since we can't determine on this level if Session443 is enabled, you should probably disable this check with control files:

sites/'. $_SERVER['SERVER_NAME'] .'/modules/allow_anon_node_add.info
sites/'. $_SERVER['SERVER_NAME'] .'/modules/disable_admin_dos_protection.info

Please create both control files, make sure that redirects are not stored in the browser cache (restart the browser or use another browser) and let me know if this helped.

Jim Kirkpatrick’s picture

Title: BOA-2.0.9 Redirects HTTPS requests to HTTP since "403 for anonymous" feature. » BOA-2.0.9 redirects HTTPS admin/edit requests to homepage on D6 due to differing Session IDs

FYI the two control files from #14 plus the current global.inc in HEAD does indeed resolve the symptom of not being able to access the admin/edit pages.

I'm still trying to work out how/if Session443 is actually changing the session cookie, or if it's essentially providing a different cookie for the HTTPS context to the HTTP one. Regardless of cause, the current global.inc script can't match the session IDs under D6 with Session443 (and perhaps other similar modules).

So I wonder if you'd consider patches for the D6 codepath along the lines of either;

  1. looking for the "LOGGED_IN" cookie Session443 sets and doing an alternate session confirmation from that? -- or
  2. just looking for any session cookie starting "SESS" and being the correct length?

In either case above we'd need a control .info file to be set for 'lower standard of session confirmation'... Or perhaps you have an alternative idea?

omega8cc’s picture

BOA doesn't check Session IDs there, only Session (cookie) names, which should always match, if the domain used is the same, no matter what - otherwise the contrib module would simply change Drupal API. We are trying to limit checks and exceptions, but if you can propose a patch, which could fix the problem for you without disabling built-in protection, that would be great.

omega8cc’s picture

Category: bug » feature
Jim Kirkpatrick’s picture

Please see my proposal around the consolidation of all control files over at #2052703: Proposal: Replace all BOA control files used in global.inc with a per-platform and per-site, well documented control.ini file.

Its related directly to this because I'm proposing a way to allow different sites to have their own session/cookie handlers and other functionalization of global.inc, all managed from a single platform- or site-specific control file...

I'll come back to this issue when the direction of discussion on the above issue is clear. Thanks.

Jim Kirkpatrick’s picture

Issue summary: View changes

adding likely cause commit, rearranged

omega8cc’s picture

Version: » 6.x-2.0-rc10
Issue summary: View changes
Status: Active » Fixed

I think that disable_admin_dos_protection = TRUE in the INI file on platform or site level should do the trick here.

You can also set this globally with line $boa_ini['disable_admin_dos_protection'] = TRUE; added in /data/conf/settings.global.inc

Status: Fixed » Closed (fixed)

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