Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.31 diff -u -p -r1.31 ajax.inc --- includes/ajax.inc 30 Apr 2010 08:07:54 -0000 1.31 +++ includes/ajax.inc 2 May 2010 19:52:01 -0000 @@ -222,10 +222,13 @@ function ajax_render($commands = array() function ajax_get_form() { $form_state = form_state_defaults(); - $form_build_id = $_POST['form_build_id']; + $form = FALSE; + if (isset($_POST['form_build_id'])) { + $form_build_id = $_POST['form_build_id']; - // Get the form from the cache. - $form = form_get_cache($form_build_id, $form_state); + // Get the form from the cache. + $form = form_get_cache($form_build_id, $form_state); + } if (!$form) { // If $form cannot be loaded from the cache, the form_build_id in $_POST // must be invalid, which means that someone performed a POST request onto Index: modules/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.27 diff -u -p -r1.27 file.module --- modules/file/file.module 1 May 2010 08:12:23 -0000 1.27 +++ modules/file/file.module 2 May 2010 19:48:33 -0000 @@ -43,8 +43,9 @@ function file_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); - $items['file/progress'] = array( + $items['file/progress/%'] = array( 'page callback' => 'file_ajax_progress', + 'page arguments' => array(2), 'delivery callback' => 'ajax_deliver', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.213 diff -u -p -r1.213 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 1 May 2010 08:12:23 -0000 1.213 +++ modules/simpletest/drupal_web_test_case.php 2 May 2010 16:58:54 -0000 @@ -1554,23 +1554,20 @@ class DrupalWebTestCase extends DrupalTe * Location of the post form. Either a Drupal path or an absolute path or * NULL to post to the current page. For multi-stage forms you can set the * path to NULL and have it post to the last received page. Example: - * * @code * // First step in form. * $edit = array(...); * $this->drupalPost('some_url', $edit, t('Save')); - * * // Second step in form. * $edit = array(...); * $this->drupalPost(NULL, $edit, t('Save')); * @endcode - * @param $edit + * @param $edit * Field data in an associative array. Changes the current input fields * (where possible) to the values indicated. A checkbox can be set to * TRUE to be checked and FALSE to be unchecked. Note that when a form * contains file upload fields, other fields cannot start with the '@' * character. - * * Multiple select fields can be set using name[] and setting each of the * possible values. Example: * @code @@ -1583,7 +1580,6 @@ class DrupalWebTestCase extends DrupalTe * example, a form may have one button with the value t('Save') and another * button with the value t('Delete'), and execute different code depending * on which one is clicked. - * * This function can also be called to emulate an AJAX submission. In this * case, this value needs to be an array with the following keys: * - path: A path to submit the form values to for AJAX-specific processing, @@ -1597,7 +1593,6 @@ class DrupalWebTestCase extends DrupalTe * generic AJAX processing path uses this to find the #ajax information * for the element, including which specific callback to use for * processing the request. - * * This can also be set to NULL in order to emulate an Internet Explorer * submission of a form with a single text field, and pressing ENTER in that * textfield: under these conditions, no button information is added to the Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.926 diff -u -p -r1.926 system.module --- modules/system/system.module 30 Apr 2010 01:33:17 -0000 1.926 +++ modules/system/system.module 2 May 2010 19:49:53 -0000 @@ -511,7 +511,7 @@ function system_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, 'file path' => 'includes', - 'file' => 'form.inc', + 'file' => 'ajax.inc', ); $items['system/timezone'] = array( 'title' => 'Time zone', Index: modules/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.124 diff -u -p -r1.124 system.test --- modules/system/system.test 20 Apr 2010 09:48:06 -0000 1.124 +++ modules/system/system.test 2 May 2010 19:38:40 -0000 @@ -1848,3 +1848,236 @@ class CompactModeTest extends DrupalWebT $this->assertTrue($this->cookies['Drupal.visitor.admin_compact_mode']['value'], t('Compact mode persists on new requests.')); } } + +/** + * Security test for XSS. + */ +class SecurityXSSTest extends DrupalWebTestCase { + + protected $xss = ""; + + public static function getInfo() { + return array( + 'name' => 'XSS', + 'description' => 'Tests XSS values.', + 'group' => 'Security', + ); + } + + function setUp() { + // Initialize double escaped XSS string. + $this->xss_double_escaped = check_plain(check_plain($this->xss)); + + // Enable all modules. + $modules = db_query('SELECT name FROM {system} WHERE type = :type AND status = :status', array( + ':type' => 'module', + ':status' => 0, + ))->fetchCol(); + call_user_func_array(array($this, 'parent::setUp'), $modules); + + // Create and log in a full-blown administrative user. + $permissions = module_invoke_all('permission'); + $this->admin_user = $this->drupalCreateUser(array_keys($permissions)); + $this->drupalLogin($this->admin_user); + + // Find all menu callbacks that are forms. + $this->form_callbacks = array(); + $this->form_callbacks_dynamic = array(); + $this->page_callbacks = array(); + foreach (menu_get_router() as $path => $item) { + // Skip default local tasks. + if ((bool) ($item['type'] & MENU_LINKS_TO_PARENT)) { + continue; + } + // Compile a list of pages containing forms. + if ($item['page callback'] == 'drupal_get_form') { + if (strpos($path, '%') !== FALSE) { + $this->form_callbacks_dynamic[$path] = $item; + } + else { + $this->form_callbacks[$path] = $item; + } + } + // @todo If the path contains or ends with "/add", also add it, but prepare + // 'page arguments'... node/add/xyz, admin/config/content/formats/add, etc. +// elseif (preg_match('@/add[/|$]@', $path)) { +// if (strpos($path, '%') !== FALSE) { +// $this->form_callbacks_dynamic[$path] = $item; +// } +// else { +// $this->form_callbacks[$path] = $item; +// } +// } + elseif (strpos($path, '%') === FALSE) { + $this->page_callbacks[$path] = $item; + } + } + } + + /** + * Test XSS attacks. + */ + function testXSS() { + // On all static paths, inject XSS where possible. + foreach ($this->form_callbacks as $path => $item) { + $this->drupalGet($path); + // Retrieve the HTML form ID. + $form_id = $item['page arguments'][0]; + $elements = $this->xpath('//input[@name=:name and @value=:value]/ancestor::form', array( + ':name' => 'form_id', + ':value' => $form_id, + )); + // Only continue if we found the form on the page; e.g., the user + // registration form throws a 403 for our user. + if (isset($elements[0])) { + $form_html_id = (string) $elements[0]['id']; + $attacked = $this->attack($form_id, $form_html_id); + // @todo After successful submit, check whether we've been redirected + // elsewhere (can be a dynamic path), and: + // - assertNoXSS() + // - assertNoDoubleEscapedXSS(); e.g., $this->xss is not double-encoded + // anywhere on the page. :) + // - try to recurse into multi-step forms, such as Field UI :) + if ($attacked) { + } + // In case we could not submit something, this most often means that we + // are on an overview-style page, such as blocks, permissions, etc. + // Such pages contain the result of our other actions, so add them to + // the page callbacks to crawl later on. + else { + $this->page_callbacks[$path] = $item; + } + } + } + // @todo Handle forms on dynamic paths...? + + // Now visit all non-form pages and ensure that XSS is not contained as raw + // HTML. + foreach ($this->page_callbacks as $path => $item) { + if ($path == 'user/logout') { + continue; + } + $this->drupalGet($path); + $this->assertNoXSS(); + } + + // Special tests: + // - maintenance page + $this->drupalLogout(); + $this->assertNoXSS(); + variable_set('site_offline', 1); + $this->drupalGet(''); + $this->assertNoXSS(); + } + + function attack($form_id, $form_html_id) { + $edit = array(); + $original = array(); + // Find any text input element. + $elements = $this->xpath('//form[@id=:id]/descendant::input[@type=:input_type]|//form[@id=:id]/descendant::textarea', array( + ':id' => $form_html_id, + ':input_type' => 'text', + )); + foreach ($elements as $element) { + $name = (string) $element['name']; + // Backup original value to handle validation errors below. + $value = (string) $element['value']; + if (drupal_strlen($value)) { + $original[$name] = $value; + } + // Insert XSS. + if ($name) { + $edit[$name] = $this->xss; + } + } + // Set custom default values for certain forms. + $this->setCustomDefaults($form_id, $edit); + + // Return if we didn't find fields to attack. + if (empty($edit)) { + return FALSE; + } + + // Identify primary submit button. + $elements = $this->xpath('//form[@id=:id]/descendant::input[@type="submit"]', array(':id' => $form_html_id)); + $button = (string) $elements[0]['value']; + $this->drupalPost(NULL, $edit, $button, array(), array(), $form_html_id); + + // Error handling; XSS values may be invalid for: + // - file system paths: Remove field value, or replace with original. + // - machine name: Remove field value; may be required though, so replace + // with "xss" for required fields. + // - custom number values: e.g., JPEG quality; will not validate unless a + // correct number is submitted; replace with original value, if any. + // - menu paths: e.g., front page; will not validate unless the path exists; + // replace with original value, if any. + + // Try to replace XSS values with 'xss' in required fields (for machine + // names) or empty strings. + $elements = $this->xpath('//input[contains(@class, :class)]', array(':class' => 'error')); + if (empty($elements)) { + return TRUE; + } + $edit2 = array(); + foreach ($elements as $element) { + $name = (string) $element['name']; + if ($name && isset($edit[$name])) { + $edit2[$name] = (strpos((string) $element['class'], 'required') !== FALSE ? 'xss' : ''); + } + } + if (empty($edit2)) { + return FALSE; + } + $this->drupalPost(NULL, $edit2, $button, array(), array(), $form_html_id); + + // If there are still errors, try to replace error fields with original + // values. + $elements = $this->xpath('//input[contains(@class, :class)]', array(':class' => 'error')); + if (empty($elements)) { + return TRUE; + } + $edit3 = array(); + foreach ($elements as $element) { + $name = (string) $element['name']; + if ($name && isset($original[$name])) { + $edit3[$name] = $original[$name]; + } + } + if (empty($edit3)) { + return FALSE; + } + $this->drupalPost(NULL, $edit3, $button, array(), array(), $form_html_id); + + $elements = $this->xpath('//input[contains(@class, :class)]', array(':class' => 'error')); + if (empty($elements)) { + return TRUE; + } + return FALSE; + } + + function assertNoXSS() { + $this->assertNoRaw($this->xss, t('Raw XSS string not found on %path.', array('%path' => $this->url))); + $this->assertNoRaw($this->xss_double_escaped, t('Double escaped XSS string not found on %path.', array('%path' => $this->url))); + } + + function setCustomDefaults($form_id, &$edit) { + switch ($form_id) { + case 'block_add_block_form': + $edit['regions[garland]'] = 'sidebar_first'; + break; + + case 'field_ui_field_overview_form': + // Unset values for "Add existing field". + foreach ($edit as $name => $value) { + if (strpos($name, '_add_existing_field') === 0) { + unset($edit[$name]); + } + } + // Default to adding a text field. + $edit['_add_new_field[field_name]'] = 'xss'; + $edit['_add_new_field[type]'] = 'text'; + $edit['_add_new_field[widget_type]'] = 'text_textfield'; + break; + } + } +} Index: modules/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.588 diff -u -p -r1.588 taxonomy.module --- modules/taxonomy/taxonomy.module 30 Apr 2010 12:52:10 -0000 1.588 +++ modules/taxonomy/taxonomy.module 2 May 2010 19:46:37 -0000 @@ -294,9 +294,10 @@ function taxonomy_menu() { 'type' => MENU_CALLBACK, 'file' => 'taxonomy.pages.inc', ); - $items['taxonomy/autocomplete'] = array( + $items['taxonomy/autocomplete/%'] = array( 'title' => 'Autocomplete taxonomy', 'page callback' => 'taxonomy_autocomplete', + 'page arguments' => array(2), 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, 'file' => 'taxonomy.pages.inc', Index: modules/taxonomy/taxonomy.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.pages.inc,v retrieving revision 1.51 diff -u -p -r1.51 taxonomy.pages.inc --- modules/taxonomy/taxonomy.pages.inc 10 Feb 2010 06:28:10 -0000 1.51 +++ modules/taxonomy/taxonomy.pages.inc 2 May 2010 19:47:45 -0000 @@ -80,7 +80,7 @@ function taxonomy_autocomplete($field_na $tags_typed = drupal_explode_tags($tags_typed); $tag_last = drupal_strtolower(array_pop($tags_typed)); - $matches = array(); + $term_matches = array(); if ($tag_last != '') { // Part of the criteria for the query come from the field's own settings. @@ -108,7 +108,6 @@ function taxonomy_autocomplete($field_na $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; - $term_matches = array(); foreach ($tags_return as $tid => $name) { $n = $name; // Term names containing commas or quotes must be wrapped in quotes.