From 675bfbddf43cae6f69315b8e91c9009a9639a006 Mon Sep 17 00:00:00 2001 From: Pantheon Automation Date: Wed, 4 Dec 2024 15:20:57 +0000 Subject: [PATCH] Update to Drupal 7.103. For more information, see https://www.drupal.org/project/drupal/releases/7.103 --- CHANGELOG.txt | 6 ++- includes/bootstrap.inc | 55 +++++++++++++++++++---- includes/common.inc | 6 ++- includes/errors.inc | 29 +++++++++++- includes/mail.inc | 2 +- includes/utility.inc | 3 +- modules/field/modules/text/text.test | 9 ++++ modules/filter/filter.module | 20 +++++++++ modules/filter/filter.test | 33 ++++++++++++++ modules/path/path.module | 1 + modules/simpletest/simpletest.pages.inc | 2 +- modules/simpletest/tests/bootstrap.test | 59 +++++++++++++++++++++++++ modules/simpletest/tests/cache.test | 14 ------ modules/simpletest/tests/error.test | 12 +++++ modules/simpletest/tests/password.test | 1 + modules/simpletest/tests/session.test | 6 ++- modules/system/system.install | 21 +++++++++ modules/system/system.mail.inc | 13 ++++-- modules/system/system.test | 15 +++++++ sites/default/default.settings.php | 35 +++++++++++++++ 20 files changed, 306 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4b7cea97f8f..2cf701b7c88 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +Drupal 7.103, 2024-12-04 +------------------------ +- So Long, and Thanks for All the Fish + Drupal 7.102, 2024-11-20 ------------------------ - Fixed security issues: @@ -5,7 +9,7 @@ Drupal 7.102, 2024-11-20 - SA-CORE-2024-008 Drupal 7.101, 2024-06-05 ------------------------ +------------------------ - Various security improvements - Various bug fixes, optimizations and improvements diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 3757479c98b..0fc307a260e 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -8,7 +8,7 @@ /** * The current system version. */ -define('VERSION', '7.102'); +define('VERSION', '7.103'); /** * Core API compatibility. @@ -457,9 +457,6 @@ abstract class DrupalCacheArray implements ArrayAccess { if ($this->bin == 'cache_form' && !variable_get('drupal_cache_array_persist_cache_form', FALSE)) { return; } - if (!is_array($this->keysToPersist)) { - throw new UnexpectedValueException(); - } $data = array(); foreach ($this->keysToPersist as $offset => $persist) { if ($persist) { @@ -732,8 +729,8 @@ function drupal_environment_initialize() { /** * Validates that a hostname (for example $_SERVER['HTTP_HOST']) is safe. * - * @return - * TRUE if only containing valid characters, or FALSE otherwise. + * @return bool + * TRUE if it only contains valid characters, FALSE otherwise. */ function drupal_valid_http_host($host) { // Limit the length of the host name to 1000 bytes to prevent DoS attacks with @@ -835,8 +832,8 @@ function drupal_settings_initialize() { // Otherwise use $base_url as session name, without the protocol // to use the same session identifiers across HTTP and HTTPS. list( , $session_name) = explode('://', $base_url, 2); - // HTTP_HOST can be modified by a visitor, but we already sanitized it - // in drupal_settings_initialize(). + // HTTP_HOST can be modified by a visitor, but we already sanitized it in + // drupal_environment_initialize(). if (!empty($_SERVER['HTTP_HOST'])) { $cookie_domain = _drupal_get_cookie_domain($_SERVER['HTTP_HOST']); } @@ -2747,6 +2744,18 @@ function _drupal_bootstrap_configuration() { // Initialize the configuration, including variables from settings.php. drupal_settings_initialize(); + // Check trusted HTTP Host headers to protect against header attacks. + if (PHP_SAPI !== 'cli') { + $host_patterns = variable_get('trusted_host_patterns', array()); + if (!empty($host_patterns)) { + if (!drupal_check_trusted_hosts($_SERVER['HTTP_HOST'], $host_patterns)) { + header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); + print 'The provided host name is not valid for this server.'; + exit; + } + } + } + // Sanitize unsafe keys from the request. DrupalRequestSanitizer::sanitize(); } @@ -3992,6 +4001,36 @@ function drupal_clear_opcode_cache($filepath) { } } +/** + * Checks trusted HTTP Host headers to protect against header injection attacks. + * + * @param string|null $host + * The host name. + * @param array $host_patterns + * The array of trusted host patterns. + * + * @return bool + * TRUE if the host is trusted, FALSE otherwise. + */ +function drupal_check_trusted_hosts($host, array $host_patterns) { + if (!empty($host) && !empty($host_patterns)) { + // Trim and remove the port number from host; host is lowercase as per + // RFC 952/2181. + $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); + + foreach ($host_patterns as $pattern) { + $pattern = sprintf('{%s}i', $pattern); + if (preg_match($pattern, $host)) { + return TRUE; + } + } + + return FALSE; + } + + return TRUE; +} + /** * Drupal's wrapper around PHP's setcookie() function. * diff --git a/includes/common.inc b/includes/common.inc index 69831b61375..f40c636dfd8 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2967,7 +2967,11 @@ function drupal_set_time_limit($time_limit) { * The path to the requested item or an empty string if the item is not found. */ function drupal_get_path($type, $name) { - return dirname(drupal_get_filename($type, $name)); + if ($filename = drupal_get_filename($type, $name)) { + return dirname($filename); + } + + return ""; } /** diff --git a/includes/errors.inc b/includes/errors.inc index 7ab84a708d2..ececd895fc4 100644 --- a/includes/errors.inc +++ b/includes/errors.inc @@ -224,7 +224,7 @@ function _drupal_log_error($error, $fatal = FALSE) { if ($fatal) { if (error_displayable($error)) { // When called from JavaScript, simply output the error message. - print t('%type: !message in %function (line %line of %file).', $error); + print t('%type: !message in %function (line %line of %file).', _drupal_strip_error_file_path($error)); } exit; } @@ -242,7 +242,7 @@ function _drupal_log_error($error, $fatal = FALSE) { $class = 'status'; } - drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class); + drupal_set_message(t('%type: !message in %function (line %line of %file).', _drupal_strip_error_file_path($error)), $class); } if ($fatal) { @@ -291,3 +291,28 @@ function _drupal_get_last_caller($backtrace) { } return $call; } + +/** + * Strip full path information from error details. + * + * @param $error + * An array with the following keys: %type, !message, %function, %file, %line + * and severity_level. + * + * @return + * An array with the same keys as the $error param but with full paths + * stripped from the %file element + */ +function _drupal_strip_error_file_path($error) { + if (!empty($error['%file'])) { + if (($drupal_root_position = strpos($error['%file'], DRUPAL_ROOT)) === 0) { + $root_length = strlen(DRUPAL_ROOT); + $error['%file'] = substr($error['%file'], $root_length + 1); + } + elseif ($drupal_root_position !== FALSE) { + // As a fallback, make sure DRUPAL_ROOT's value is not in the path. + $error['%file'] = str_replace(DRUPAL_ROOT, 'DRUPAL_ROOT', $error['%file']); + } + } + return $error; +} diff --git a/includes/mail.inc b/includes/mail.inc index a97c788f09e..0da9209f789 100644 --- a/includes/mail.inc +++ b/includes/mail.inc @@ -624,7 +624,7 @@ function drupal_mail_format_display_name($string) { */ function _drupal_wrap_mail_line(&$line, $key, $values) { // Use soft-breaks only for purely quoted or unindented text. - $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); + $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); // Break really long words at the maximum width allowed. $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n", TRUE); } diff --git a/includes/utility.inc b/includes/utility.inc index f651fd63126..5c2365e0c39 100644 --- a/includes/utility.inc +++ b/includes/utility.inc @@ -26,7 +26,7 @@ function drupal_var_export($var, $prefix = '') { // Don't export keys if the array is non associative. $export_keys = array_values($var) != $var; foreach ($var as $key => $value) { - $output .= ' ' . ($export_keys ? drupal_var_export($key) . ' => ' : '') . drupal_var_export($value, ' ', FALSE) . ",\n"; + $output .= ' ' . ($export_keys ? drupal_var_export($key) . ' => ' : '') . drupal_var_export($value, ' ') . ",\n"; } $output .= ')'; } @@ -35,7 +35,6 @@ function drupal_var_export($var, $prefix = '') { $output = $var ? 'TRUE' : 'FALSE'; } elseif (is_string($var)) { - $line_safe_var = str_replace("\n", '\n', $var); if (strpos($var, "\n") !== FALSE || strpos($var, "'") !== FALSE) { // If the string contains a line break or a single quote, use the // double quote export mode. Encode backslash and double quotes and diff --git a/modules/field/modules/text/text.test b/modules/field/modules/text/text.test index 3da56f0cb22..2303cb18e27 100644 --- a/modules/field/modules/text/text.test +++ b/modules/field/modules/text/text.test @@ -377,6 +377,15 @@ class TextSummaryTestCase extends DrupalWebTestCase { // Test text_summary() for different sizes. for ($i = 0; $i <= 37; $i++) { $this->callTextSummary($text, $expected[$i], NULL, $i); + + // libxml2 library changed parsing behavior on version 2.9.14. Skip + // specific edge-case testing for all further versions. + // @see https://gitlab.gnome.org/GNOME/libxml2/-/issues/474 + // @see https://www.drupal.org/project/drupal/issues/3397882 + if ($i == 1 && defined('LIBXML_VERSION') && LIBXML_VERSION >= 20914) { + continue; + } + $this->callTextSummary($text, $expected_lb[$i], 'plain_text', $i); $this->callTextSummary($text, $expected_lb[$i], 'filtered_html', $i); } diff --git a/modules/filter/filter.module b/modules/filter/filter.module index 7c8b2b88cfa..103d066b75d 100644 --- a/modules/filter/filter.module +++ b/modules/filter/filter.module @@ -1515,14 +1515,26 @@ function _filter_url($text, $filter) { // re-split after each task, since all injected HTML tags must be correctly // protected before the next task. foreach ($tasks as $task => $pattern) { + // Store the current text in case any of the preg_* functions fail. + $saved_text = $text; + // HTML comments need to be handled separately, as they may contain HTML // markup, especially a '>'. Therefore, remove all comment contents and add // them back later. _filter_url_escape_comments('', TRUE); $text = preg_replace_callback('``s', '_filter_url_escape_comments', $text); + if (preg_last_error()) { + $text = $saved_text; + continue 1; + } // Split at all tags; ensures that no tags or attributes are processed. $chunks = preg_split('/(<.+?>)/is', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + if (preg_last_error()) { + $text = $saved_text; + continue 1; + } + // PHP ensures that the array consists of alternating delimiters and // literals, and begins and ends with a literal (inserting NULL as // required). Therefore, the first chunk is always text: @@ -1539,6 +1551,10 @@ function _filter_url($text, $filter) { // If there is a match, inject a link into this chunk via the callback // function contained in $task. $chunks[$i] = preg_replace_callback($pattern, $task, $chunks[$i]); + if (preg_last_error()) { + $text = $saved_text; + continue 2; + } } // Text chunk is done, so next chunk must be a tag. $chunk_type = 'tag'; @@ -1566,6 +1582,10 @@ function _filter_url($text, $filter) { // Revert back to the original comment contents _filter_url_escape_comments('', FALSE); $text = preg_replace_callback('``', '_filter_url_escape_comments', $text); + if (preg_last_error()) { + $text = $saved_text; + continue 1; + } } return $text; diff --git a/modules/filter/filter.test b/modules/filter/filter.test index 2af3103c124..9f7c3f5eb08 100644 --- a/modules/filter/filter.test +++ b/modules/filter/filter.test @@ -1637,6 +1637,7 @@ www.example.com with a newline in comments --> * comments. * - Empty HTML tags (BR, IMG). * - Mix of absolute and partial URLs, and e-mail addresses in one content. + * - Input that exceeds PCRE backtracking limit. */ function testUrlFilterContent() { // Setup dummy filter object. @@ -1650,6 +1651,16 @@ www.example.com with a newline in comments --> $expected = file_get_contents($path . '/filter.url-output.txt'); $result = _filter_url($input, $filter); $this->assertIdentical($result, $expected, 'Complex HTML document was correctly processed.'); + + // Case of a small and simple HTML document. + $input = $expected = '

www.test.com

'; + $result = $this->filterUrlWithPcreErrors($input, $filter); + $this->assertIdentical($expected, $result, 'Simple HTML document was left intact when PCRE errors occurred.'); + + // Case of a complex HTML document. + $input = $expected = file_get_contents($path . '/filter.url-input.txt'); + $result = $this->filterUrlWithPcreErrors($input, $filter); + $this->assertIdentical($expected, $result, 'Complex HTML document was left intact when PCRE errors occurred.'); } /** @@ -1890,6 +1901,28 @@ body {color:red} function assertNoNormalized($haystack, $needle, $message = '', $group = 'Other') { return $this->assertTrue(strpos(strtolower(decode_entities($haystack)), $needle) === FALSE, $message, $group); } + + /** + * Calls filter_url with pcre.backtrack_limit set to 1. + * + * When PCRE errors occur, _filter_url() returns the input text unchanged. + * + * @param $input + * Text to pass on to _filter_url(). + * @param $filter + * Filter to pass on to _filter_url(). + * @return + * The processed $input. + */ + protected function filterUrlWithPcreErrors($input, $filter) { + $pcre_backtrack_limit = ini_get('pcre.backtrack_limit'); + // Setting this limit to the smallest possible value should cause PCRE + // errors and break the various preg_* functions used by _filter_url(). + ini_set('pcre.backtrack_limit', 1); + $result = _filter_url($input, $filter); + ini_set('pcre.backtrack_limit', $pcre_backtrack_limit); + return $result; + } } /** diff --git a/modules/path/path.module b/modules/path/path.module index 4614b0fa222..35a8fa56cc0 100644 --- a/modules/path/path.module +++ b/modules/path/path.module @@ -41,6 +41,7 @@ function path_permission() { return array( 'administer url aliases' => array( 'title' => t('Administer URL aliases'), + 'restrict access' => TRUE, ), 'create url aliases' => array( 'title' => t('Create and edit URL aliases'), diff --git a/modules/simpletest/simpletest.pages.inc b/modules/simpletest/simpletest.pages.inc index 3127459e2f7..c6f6a07b529 100644 --- a/modules/simpletest/simpletest.pages.inc +++ b/modules/simpletest/simpletest.pages.inc @@ -443,7 +443,7 @@ function simpletest_settings_form($form, &$form_state) { $form['general']['simpletest_clear_results'] = array( '#type' => 'checkbox', '#title' => t('Clear results after each complete test suite run'), - '#description' => t('By default SimpleTest will clear the results after they have been viewed on the results page, but in some cases it may be useful to leave the results in the database. The results can then be viewed at admin/config/development/testing/[test_id]. The test ID can be found in the database, simpletest table, or kept track of when viewing the results the first time. Additionally, some modules may provide more analysis or features that require this setting to be disabled.'), + '#description' => t('By default SimpleTest will clear the results after they have been viewed on the results page, but in some cases it may be useful to leave the results in the database. The results can then be viewed at admin/config/development/testing/results/[test_id]. The test ID can be found in the database, simpletest table, or kept track of when viewing the results the first time. Additionally, some modules may provide more analysis or features that require this setting to be disabled.'), '#default_value' => variable_get('simpletest_clear_results', TRUE), ); $form['general']['simpletest_verbose'] = array( diff --git a/modules/simpletest/tests/bootstrap.test b/modules/simpletest/tests/bootstrap.test index a332955861e..5d846f621a0 100644 --- a/modules/simpletest/tests/bootstrap.test +++ b/modules/simpletest/tests/bootstrap.test @@ -963,3 +963,62 @@ class BootstrapDrupalCacheArrayTestCase extends DrupalWebTestCase { $this->assertTrue(is_string($payload2) && (strpos($payload2, 'phpinfo') !== FALSE), 'DrupalCacheArray persisted data to cache_form.'); } } + +/** + * Test the trusted HTTP host configuration. + */ +class BootstrapTrustedHostsTestCase extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Trusted HTTP host test', + 'description' => 'Tests the trusted_host_patterns configuration.', + 'group' => 'Bootstrap', + ); + } + + /** + * Tests hostname validation. + * + * @see drupal_check_trusted_hosts() + */ + function testTrustedHosts() { + $trusted_host_patterns = array( + '^example\.com$', + '^.+\.example\.com$', + '^example\.org', + '^.+\.example\.org', + ); + + foreach ($this->providerTestTrustedHosts() as $data) { + $test = array_combine(array('host', 'message', 'expected'), $data); + $valid_host = drupal_check_trusted_hosts($test['host'], $trusted_host_patterns); + $this->assertEqual($test['expected'], $valid_host, $test['message']); + } + } + + /** + * Provides test data for testTrustedHosts(). + */ + public function providerTestTrustedHosts() { + $data = array(); + + // Tests canonical URL. + $data[] = array('www.example.com', 'canonical URL is trusted', TRUE); + + // Tests missing hostname for HTTP/1.0 compatability where the Host + // header is optional. + $data[] = array(NULL, 'empty Host is valid', TRUE); + + // Tests the additional patterns from the settings. + $data[] = array('example.com', 'host from settings is trusted', TRUE); + $data[] = array('subdomain.example.com', 'host from settings is trusted', TRUE); + $data[] = array('www.example.org', 'host from settings is trusted', TRUE); + $data[] = array('example.org', 'host from settings is trusted', TRUE); + + // Tests mismatch. + $data[] = array('www.blackhat.com', 'unspecified host is untrusted', FALSE); + + return $data; + } +} diff --git a/modules/simpletest/tests/cache.test b/modules/simpletest/tests/cache.test index b42de360b4e..3edc67102c1 100644 --- a/modules/simpletest/tests/cache.test +++ b/modules/simpletest/tests/cache.test @@ -303,20 +303,6 @@ class CacheClearCase extends CacheTestCase { $this->assertTrue($this->checkCacheExists('test_cid_clear3', $this->default_value), 'Entry was not cleared from the cache'); - - // Set the cache clear threshold to 2 to confirm that the full bin is cleared - // when the threshold is exceeded. - variable_set('cache_clear_threshold', 2); - cache_set('test_cid_clear1', $this->default_value, $this->default_bin); - cache_set('test_cid_clear2', $this->default_value, $this->default_bin); - $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) - && $this->checkCacheExists('test_cid_clear2', $this->default_value), - 'Two cache entries were created.'); - cache_clear_all(array('test_cid_clear1', 'test_cid_clear2', 'test_cid_clear3'), $this->default_bin); - $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) - || $this->checkCacheExists('test_cid_clear2', $this->default_value) - || $this->checkCacheExists('test_cid_clear3', $this->default_value), - 'All cache entries removed when the array exceeded the cache clear threshold.'); } /** diff --git a/modules/simpletest/tests/error.test b/modules/simpletest/tests/error.test index 5b56d52d9ba..1a6c293034a 100644 --- a/modules/simpletest/tests/error.test +++ b/modules/simpletest/tests/error.test @@ -103,6 +103,15 @@ class DrupalErrorHandlerTestCase extends DrupalWebTestCase { function assertErrorMessage(array $error) { $message = t('%type: !message in %function (line ', $error); $this->assertRaw($message, format_string('Found error message: !message.', array('!message' => $message))); + + // Also check that no full path from the error is displayed. + $this->assertNoRaw($error['%file'], format_string('Full path from error not displayed: %file.', array('%file' => $error['%file']))); + + // Check that the path was displayed with the DRUPAL_ROOT hidden. + $root_length = strlen(DRUPAL_ROOT); + $stripped_path = substr($error['%file'], $root_length + 1); + $sanitized_path = t('of %path)', array('%path' => $stripped_path)); + $this->assertRaw($sanitized_path, 'Path in error message was sanitized.'); } /** @@ -111,5 +120,8 @@ class DrupalErrorHandlerTestCase extends DrupalWebTestCase { function assertNoErrorMessage(array $error) { $message = t('%type: !message in %function (line ', $error); $this->assertNoRaw($message, format_string('Did not find error message: !message.', array('!message' => $message))); + + // Also check that no full path from the error is displayed. + $this->assertNoRaw($error['%file'], format_string('Full path from error not displayed: %file.', array('%file' => $error['%file']))); } } diff --git a/modules/simpletest/tests/password.test b/modules/simpletest/tests/password.test index 7105f3b7add..489b191fad7 100644 --- a/modules/simpletest/tests/password.test +++ b/modules/simpletest/tests/password.test @@ -73,6 +73,7 @@ class PasswordHashingTest extends DrupalWebTestCase { $result = user_hash_password($password); $this->assertFalse(empty($result), '510 byte long password is allowed.'); $password .= 'xx'; + $result = user_hash_password($password); $this->assertFalse(empty($result), '512 byte long password is allowed.'); $password = str_repeat('€', 171); $result = user_hash_password($password); diff --git a/modules/simpletest/tests/session.test b/modules/simpletest/tests/session.test index e21d14a1418..c92fac9f0d8 100644 --- a/modules/simpletest/tests/session.test +++ b/modules/simpletest/tests/session.test @@ -779,8 +779,10 @@ class SessionHttpsTestCase extends DrupalWebTestCase { $form[0]['action'] = $this->httpsUrl('user'); $this->drupalPost(NULL, $edit, t('Log in')); - // Make the secure session cookie blank. - curl_setopt($this->curlHandle, CURLOPT_COOKIE, "$secure_session_name="); + // Make the secure session cookie blank. Closing the curl handler will stop + // the previous session ID from persisting. + $this->curlClose(); + $this->additionalCurlOptions[CURLOPT_COOKIE] = rawurlencode($secure_session_name) . '=;'; $this->drupalGet($this->httpsUrl('user')); $this->assertNoText($admin_user->name, 'User is not logged in as admin'); $this->assertNoText($standard_user->name, "The user's own name is not displayed because the invalid session cookie has logged them out."); diff --git a/modules/system/system.install b/modules/system/system.install index 76f5473d081..747745c4122 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -579,6 +579,27 @@ function system_requirements($phase) { } } + // See if trusted hostnames have been configured, and warn the user if they + // are not set. + if ($phase == 'runtime') { + $trusted_host_patterns = variable_get('trusted_host_patterns', array()); + if (empty($trusted_host_patterns)) { + $requirements['trusted_host_patterns'] = array( + 'title' => $t('Trusted Host Settings'), + 'value' => $t('Not enabled'), + 'description' => $t('The trusted_host_patterns setting is not configured in settings.php. This can lead to security vulnerabilities. It is highly recommended that you configure this. See Protecting against HTTP HOST Header attacks for more information.', array('@url' => 'https://www.drupal.org/node/1992030')), + 'severity' => REQUIREMENT_ERROR, + ); + } + else { + $requirements['trusted_host_patterns'] = array( + 'title' => $t('Trusted Host Settings'), + 'value' => $t('Enabled'), + 'description' => $t('The trusted_host_patterns setting is set to allow %trusted_host_patterns', array('%trusted_host_patterns' => implode(', ', $trusted_host_patterns))), + ); + } + } + return $requirements; } diff --git a/modules/system/system.mail.inc b/modules/system/system.mail.inc index 246fc573217..364cf02007b 100644 --- a/modules/system/system.mail.inc +++ b/modules/system/system.mail.inc @@ -21,10 +21,8 @@ class DefaultMailSystem implements MailSystemInterface { public function format(array $message) { // Join the body array into one string. $message['body'] = implode("\n\n", $message['body']); - // Convert any HTML to plain-text. + // Convert any HTML to plain-text and wrap the mail body for sending. $message['body'] = drupal_html_to_text($message['body']); - // Wrap the mail body for sending. - $message['body'] = drupal_wrap_mail($message['body']); return $message; } @@ -64,7 +62,14 @@ class DefaultMailSystem implements MailSystemInterface { $mail_body = preg_replace('@\r?\n@', $line_endings, $message['body']); // For headers, PHP's API suggests that we use CRLF normally, // but some MTAs incorrectly replace LF with CRLF. See #234403. - $mail_headers = join("\n", $mimeheaders); + $headers_line_endings = variable_get('mail_headers_line_endings', "\n"); + if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 80000 ) { + // PHP 8+ requires headers to be separated by CRLF, see: + // - https://bugs.php.net/bug.php?id=81158 + // - https://github.com/php/php-src/commit/6983ae751cd301886c966b84367fc7aaa1273b2d#diff-c6922cd89f6f75912eb377833ca1eddb7dd41de088be821024b8a0e340fed3df + $headers_line_endings = variable_get('mail_headers_line_endings', "\r\n"); + } + $mail_headers = join($headers_line_endings, $mimeheaders); // We suppress warnings and notices from mail() because of issues on some // hosts. The return value of this method will still indicate whether mail diff --git a/modules/system/system.test b/modules/system/system.test index 518892d63fb..312b90a7940 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -2937,6 +2937,21 @@ class SystemAdminTestCase extends DrupalWebTestCase { $this->drupalGet(''); $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], 'Compact mode persists on new requests.'); } + + /** + * Test Trusted Host Settings message on the status report page. + */ + function testTrustedHostSettingsMessage() { + $this->drupalGet('admin/reports/status'); + $this->assertText('The trusted_host_patterns setting is not configured in settings.php.'); + $this->assertNoText('The trusted_host_patterns setting is set to allow'); + + variable_set('trusted_host_patterns', array('a_trusted_pattern', 'another_trusted_pattern')); + $this->drupalGet('admin/reports/status'); + $this->assertNoText('The trusted_host_patterns setting is not configured in settings.php.'); + $this->assertText('The trusted_host_patterns setting is set to allow a_trusted_pattern, another_trusted_pattern'); + } + } /** diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index fb6040c6ff5..dbab92f6f90 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -639,6 +639,41 @@ */ # $conf['allow_authorize_operations'] = FALSE; +/** + * Trusted host configuration. + * + * Drupal can attempt to prevent HTTP Host header spoofing. + * + * To enable the trusted host mechanism, you enable your allowable hosts in + * $conf['trusted_host_patterns']. This should be an array of regular expression + * patterns, without delimiters, representing the hosts you would like to allow. + * + * For example, this code will allow the site to only run from www.example.com. + * + * @code + * $conf['trusted_host_patterns'] = array( + * '^www\.example\.com$', + * ); + * @endcode + * + * If you are running multisite, or if you are running your site from different + * domain names (for example, you don't redirect http://www.example.com to + * http://example.com), you should specify all of the host patterns that are + * allowed by your site. + * + * For example, this code will allow the site to run off of all variants of + * example.com and example.org, with all subdomains included. + * + * @code + * $conf['trusted_host_patterns'] = array( + * '^example\.com$', + * '^.+\.example\.com$', + * '^example\.org', + * '^.+\.example\.org', + * ); + * @endcode + */ + /** * Theme debugging: *