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.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 18:23:56 -0000
@@ -1848,3 +1848,159 @@ 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 = "<script>alert('XSS');</script>";
+
+  public static function getInfo() {
+    return array(
+      'name' => 'XSS',
+      'description' => 'Tests XSS values.',
+      'group' => 'Security',
+    );
+  }
+
+  function setUp() {
+    // 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;
+        }
+      }
+      elseif (strpos($path, '%') === FALSE) {
+        $this->page_callbacks[$path] = $item;
+      }
+    }
+  }
+
+  /**
+   * Test XSS attacks.
+   */
+  function testXSS() {
+    // On all static paths, inject XSS where possible.
+    $i = 0;
+    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);
+//        if ($attacked && ++$i >= 5) {
+//          break;
+//        }
+      }
+    }
+    // @todo Handle forms on dynamic paths...?
+
+    // Now visit all non-form pages and ensure that XSS is not contained as raw
+    // HTML.
+    $i = 0;
+    foreach ($this->page_callbacks as $path => $item) {
+      $this->drupalGet($path);
+      $this->assertNoXSS();
+//      if (++$i >= 5) {
+//        break;
+//      }
+    }
+  }
+
+  function attack($form_id, $form_html_id) {
+    $edit = array();
+    // Set custom default values for certain forms.
+    $this->setCustomDefaults($form_id, $edit);
+
+    // 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'];
+      if ($name) {
+        $edit[$name] = $this->xss;
+      }
+    }
+    // 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 value did not pass custom validation handlers, such as
+    //   - file paths: Remove field value.
+    //   - machine name: Remove field value; may be required though, so replace
+    //     with "xss" for required fields.
+    $edit2 = array();
+    $elements = $this->xpath('//input[contains(@class, :class)]', array(
+      ':class' => 'error',
+    ));
+    foreach ($elements as $element) {
+      $name = (string) $element['name'];
+      if ($name && isset($edit[$name])) {
+        if (strpos((string) $element['class'], 'required') !== FALSE) {
+          $edit2[$name] = 'xss';
+        }
+        else {
+          $edit2[$name] = '';
+        }
+      }
+    }
+    if (!empty($edit2)) {
+      $this->drupalPost(NULL, $edit2, $button, array(), array(), $form_html_id);
+    }
+
+    return TRUE;
+  }
+
+  function assertNoXSS() {
+    $this->assertNoRaw($this->xss, 'Raw XSS input not found in HTML.');
+  }
+
+  function setCustomDefaults($form_id, &$edit) {
+    switch ($form_id) {
+      case 'block_add_block_form':
+        $edit['regions[garland]'] = 'sidebar_first';
+        break;
+    }
+  }
+}
