Problem/Motivation

Cron.php does not check for maintenance mode correctly and causes drupal_deliver_html_page() to call drupal_render_page() with an integer argument under some circumstances.

The problem is when a site has a custom 403 page set, maintenance mode is active and cron.php is accessed.

1. cron.php will call drupal_access_denied().
2. drupal_access_denied() will call drupal_deliver_html_page().
3. And drupal_deliver_html_page() will fail to recognize the maintenance mode, as the $return variable will have a value of MENU_SITE_OFFLINE:

      case MENU_ACCESS_DENIED:
        // Print a 403 page.
        drupal_add_http_header('Status', '403 Forbidden');
        watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);

        // Keep old path for reference, and to allow forms to redirect to it.
        if (!isset($_GET['destination'])) {
          // Make sure that the current path is not interpreted as external URL.
          if (!url_is_external($_GET['q'])) {
            $_GET['destination'] = $_GET['q'];
          }
        }

        $path = drupal_get_normal_path(variable_get('site_403', ''));
        if ($path && $path != $_GET['q']) {
          // Custom 403 handler. Set the active item in case there are tabs to
          // display or other dependencies on the path.
          menu_set_active_item($path);
          $return = menu_execute_active_handler($path, FALSE);
        }

        if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
          // Standard 403 handler.
          drupal_set_title(t('Access denied'));
          $return = t('You are not authorized to access this page.');
        }

        print drupal_render_page($return);
        break;

This will cause errors:

Cannot use a scalar value as an array block.module:271 
Invalid argument supplied for foreach() common.inc:6594         
Cannot use a scalar value as an array common.inc:6051      
Cannot use a scalar value as an array common.inc:6106

Steps to reproduce

Start with a clean site (ie. minimal profile), then:

  1. Switch on maintenance_mode drush vset maintenance_mode 1
  2. Set a custom access denied page drush vset site_403 'non-empty'
  3. Access the cron.php of your site curl https://example.com/cron.php

The result is this set of errors:

Cannot use a scalar value as an array block.module:271                                                                                                                                                   
Invalid argument supplied for foreach() common.inc:6594                                                                                                                                                  
Cannot use a scalar value as an array common.inc:6051                                                                                                                                                   
Cannot use a scalar value as an array common.inc:6106

Depending on what othen modules you have activated there might be even more errors.

Proposed resolution

Fix the conditions in cron.php to return drupal_site_offline() instead of drupal_access_denied() when the maintenance mode is active.

==== Original report ====

Summary

In drupal_deliver_html_page() in some circumstances menu_execute_active_handler() is called and its return value is directly passed to drupal_render_page() even if it is an integer

Steps to reproduce

Start with a clean site (ie. minimal profile), then:

  1. Switch on maintenance_mode drush vset maintenance_mode 1
  2. Set a custom access denied page drush vset site_403 'non-empty'
  3. Access the cron.php of your site curl https://example.com/cron.php

The result is this set of errors:

Cannot use a scalar value as an array block.module:271                                                                                                                                                   
Invalid argument supplied for foreach() common.inc:6594                                                                                                                                                  
Cannot use a scalar value as an array common.inc:6051                                                                                                                                                   
Cannot use a scalar value as an array common.inc:6106

Depending on what othen modules you have activated there might be even more errors.

Analysis

drupal_deliver_html_page() checks only for MENU_NOT_FOUND and MENU_ACCESS_DENIED before passing the value to drupal_render_page(). This is not sufficent.

Proposed solution

Check for is_int($return) instead of just specific integers.

Comments

torotil created an issue. See original summary.

torotil’s picture

Status: Active » Needs review
StatusFileSize
new1.07 KB

Here is an attempt to fix this.

torotil’s picture

Issue summary: View changes
torotil’s picture

Issue summary: View changes
torotil’s picture

Issue summary: View changes
damienmckenna’s picture

With this step:

Set a custom access denied page drush vset site_403 something

What is something? Is it a node system path, e.g. "node/123"? Is it a node alias? Is it the system path for a custom menu item? This might be the cause of the problem.

torotil’s picture

Hi Damien,

the steps to reproduce are complete if you take the commands literally. It doesn’t matter what’s the actual value of site_403 as long as it’s not empty (or more precisely drupal_get_normal_path(variable_get('site_403'), '') must be non-empty). It’s simply about this block of code:


        $path = drupal_get_normal_path(variable_get('site_403', ''));
        if ($path && $path != $_GET['q']) {
          // Custom 403 handler. Set the active item in case there are tabs to
          // display or other dependencies on the path.
          menu_set_active_item($path);
          $return = menu_execute_active_handler($path, FALSE);
        }

        if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
          // Standard 403 handler.
          drupal_set_title(t('Access denied'));
          $return = t('You are not authorized to access this page.');
        }

        print drupal_render_page($return);

If the first if is executed, but the site is in maintenance mode then menu_execute_active_handler($path, FALSE); returns MENU_SITE_OFFLINE (=4). The second if is not executed. Then drupal_render_page(4) is called.

torotil’s picture

Issue summary: View changes
Jorrit’s picture

Status: Needs review » Reviewed & tested by the community

To clarify the problem: menu_execute_active_handler() may return MENU_SITE_OFFLINE and this is passed directly to drupal_render_page() in drupal_deliver_html_page().

The patch works for any case where the above situation happens.

A different solution for the specific case that torotil and I encounter would be to replace drupal_access_denied() with drupal_site_offline() on line 22 of cron.php.

poker10’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new1.61 KB
new2.53 KB

Thanks for the patch and report. I think that the menu_execute_active_handler() can hypothetically return other integers via this part of the code:

$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);

So changing that condition to is_int($return) could potentially cause problems in other edge cases. I have tested this and it seems reproducible only with cron.php - if you visit any other URL (existing, non-existing, or without access) then the correct maintenance page is returned in each case. So I think it will be better to fix this directly in cron.php.

A different solution for the specific case that torotil and I encounter would be to replace drupal_access_denied() with drupal_site_offline() on line 22 of cron.php.

Yes, but we cannot change both drupal_access_denied() in cron.php to the drupal_site_offline(), because in the regular scenario (without maintenance mode) the user should get an access denied when the cron_key is empty / invalid. But what if we switch the conditions in cron.php to check the maintenance mode first and then only the cron_key? Then we can change the first drupal_access_denied() to the drupal_site_offline() and also keep the second drupal_access_denied(). See my proposed patch (not attaching an interdiff as my changes are completely different). Let's check if this passes the tests.

The last submitted patch, 10: 3007538-10_test-only.patch, failed testing. View results

poker10’s picture

Issue tags: +Pending Drupal 7 commit

Adding a tag for the other maintainer review.

poker10’s picture

mcdruid’s picture

Status: Needs review » Reviewed & tested by the community

This LGTM (but I am not the Framework Manager!)

poker10 credited Fabianx.

poker10’s picture

Title: drupal_deliver_html_page() might call drupal_render_page() with an integer argument. » Cron.php does not check for maintenance mode correctly
Issue summary: View changes
Issue tags: -Pending Drupal 7 commit, -Needs framework manager review +RTBM

Discussed this on Slack with other D7 maintainers and @Fabianx reviewed this as well:

This one is fine to go in, but IS + Title should say something like:
“Cron.php does not check for maintenance mode correctly”.
The patch itself is fine. An easy oversight that elseif is wrong.
And also why I am a fan of early returns.

We will fix the issue in the cron.php, as this was the only place where the problem was reproduced and it should be a safer approach. If another usecase would appear in the future, we will take a look at that separately.

Updated the IS and title, over to @mcdruid for commit. Thanks!

  • mcdruid committed b2805561 on 7.x
    Issue #3007538 by poker10, torotil, DamienMcKenna, Jorrit, Fabianx: Cron...
mcdruid’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -RTBM

Thank you everybody!

Status: Fixed » Closed (fixed)

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