Skip to content

Commit

Permalink
Update to Drupal 7.103. For more information, see https://www.drupal.…
Browse files Browse the repository at this point in the history
  • Loading branch information
Pantheon Automation committed Dec 5, 2024
1 parent 34b0406 commit c4ecddb
Show file tree
Hide file tree
Showing 20 changed files with 306 additions and 36 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
Drupal 7.103, 2024-12-04
------------------------
- So Long, and Thanks for All the Fish

Drupal 7.102, 2024-11-20
------------------------
- Fixed security issues:
- SA-CORE-2024-005
- SA-CORE-2024-008

Drupal 7.101, 2024-06-05
-----------------------
------------------------
- Various security improvements
- Various bug fixes, optimizations and improvements

Expand Down
55 changes: 47 additions & 8 deletions includes/bootstrap.inc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/**
* The current system version.
*/
define('VERSION', '7.102');
define('VERSION', '7.103');

/**
* Core API compatibility.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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']);
}
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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.
*
Expand Down
6 changes: 5 additions & 1 deletion includes/common.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}

/**
Expand Down
29 changes: 27 additions & 2 deletions includes/errors.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion includes/mail.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 1 addition & 2 deletions includes/utility.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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 .= ')';
}
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions modules/field/modules/text/text.test
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
20 changes: 20 additions & 0 deletions modules/filter/filter.module
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions modules/filter/filter.test
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 = '<p>www.test.com</p>';
$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.');
}

/**
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions modules/path/path.module
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion modules/simpletest/simpletest.pages.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <em>admin/config/development/testing/[test_id]</em>. 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 <em>admin/config/development/testing/results/[test_id]</em>. 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(
Expand Down
59 changes: 59 additions & 0 deletions modules/simpletest/tests/bootstrap.test
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading

0 comments on commit c4ecddb

Please sign in to comment.