Drupal Workflow 6.x-1.1 and 5.x-2.3 XSS Vulnerability

30 November -0001

Description of Vulnerability:

Drupal (http://drupal.org) is a robust content management system (CMS) written in PHP and MySQL that provides extensibility through various third party modules. The Workflow module (http://drupal.org/project/workflow) "allows the creation and assignment of arbitrary workflows to Drupal node types. Workflows are made up of workflow states. For example, a workflow with the states Draft, Review, and Published could be assigned to the Story node type."

The Workflow module versions 6.x-1.1 and 5.x-2.3 contain a cross site scripting vulnerability.

Systems affected:

Drupal 6.14 with Workflow 6.x-1.1 and Drupal 5.20 with Workflow 5.x-2.3 were tested and shown to be vulnerable.

Impact:

XSS vulnerabilities may expose site administrative accounts to compromise which could lead to web server process compromise.

Mitigating factors:

The Workflow module must be installed. To carry out a Workflow based XSS exploit the attacker must have 'administer workflow' or 'administer content types' or 'administer users' permissions.

Proof of Concept:

1. Install Drupal 6.14 2. Install Workflow 6.x-1.1 3. Enable the Workflow module from Administer -> Site building -> Modules 4. Click Administer -> Site building -> Workflow 5. Click 'Add workflow' 6. Enter "<script>alert('xss');</script>" in the 'Workflow Name:' textbox and click 'Add workflow 7. Enter "<script>alert('state xss');</script> in the 'State name' on the resulting screen and click 'Save' 8. On the resulting screen (Administer -> Site building -> Workflow) the workflows are listed and the JavaScript is rendered producing alerts for workflow names, state names, and content type names. 9. Click 'Edit' next to any workflow name to view JavaScript rendered as a result of any malicious role name.

Technical details:

The Workflow module fails to sanitize the output of the Workflow name, state name, role names, and content type names before display. Applying the following patch fixes this vulnerability.

Patch for Workflow 6.x-1.1

Applying the following patch mitigates these threats in Workflow 6.x-1.1.

diff -up workflow/workflow_access.module workflow/workflow_access.module
--- workflow/workflow_access.module     2008-08-17 23:04:13.000000000 -0400
+++ workflow/workflow_access.module       2009-10-12 14:59:59.330102222 -0400
@@ -54,7 +54,7 @@ function workflow_access_node_access_rec
 function workflow_access_form_workflow_edit_form_alter(&$form, $form_state) {
   // A list of roles available on the site and our 
   // special -1 role used to represent the node author.
-  $rids = user_roles();
+  $rids = array_map('filter_xss', user_roles()); 
   $rids['-1'] = t('author');
   
   $form['workflow_access'] = array(
@@ -99,7 +99,7 @@ function workflow_access_form_workflow_e
     // TODO better tables using a #theme function instead of direct #prefixing
     $form['workflow_access'][$sid] = array(
       '#type' => 'fieldset', 
-      '#title' => $state,
+      '#title' => filter_xss($state),
       '#collapsible' => TRUE,
       '#tree' => TRUE,
     );
diff -up workflow/workflow.admin.inc workflow/workflow.admin.inc
--- workflow/workflow.admin.inc 2009-01-01 15:33:20.000000000 -0500
+++ workflow/workflow.admin.inc   2009-10-12 14:51:15.833106030 -0400
@@ -248,9 +248,9 @@ function theme_workflow_edit_form($form)
     foreach ($states as $state_id => $name) {
       // Don't allow transition TO (creation).
       if ($name != t('(creation)')) {
-        $header[] = array('data' => t($name));
+        $header[] = array('data' => filter_xss(t($name)));
       }
-      $row = array(array('data' => $name));
+      $row = array(array('data' => filter_xss($name)));
       foreach ($states as $nested_state_id => $nested_name) {
         if ($nested_name == t('(creation)')) {
           // Don't allow transition TO (creation).
@@ -507,7 +507,7 @@ function workflow_overview() {
       unset($links['workflow_overview_actions']);
     }
 
-    $row[] = array($name, theme('links', $links));
+    $row[] = array(filter_xss($name), theme('links', $links));
     $subrows = array();
 
     foreach ($states as $sid => $state_name) {
@@ -526,7 +526,7 @@ function workflow_overview() {
       }
       // Allow modules to insert state operations.
       $state_links = array_merge($state_links, module_invoke_all('workflow_operations', 'state', $wid, $sid));
-      $subrows[] = array(t($state_name), theme('links', $state_links));
+      $subrows[] = array(filter_xss(t($state_name)), theme('links', $state_links));
       unset($state_links);
     }
 
@@ -643,7 +643,7 @@ function workflow_types_form() {
   foreach (node_get_types('names') as $type => $name) {
     $form[$type]['workflow'] = array(
       '#type' => 'select',
-      '#title' => $name,
+      '#title' => filter_xss($name),
       '#options' => $workflows,
       '#default_value' => isset($type_map[$type]) ? $type_map[$type] : 0
     );
diff -up workflow/workflow.module workflow/workflow.module
--- workflow/workflow.module    2009-01-01 16:09:16.000000000 -0500
+++ workflow/workflow.module      2009-10-12 14:56:41.737309685 -0400
@@ -1345,7 +1345,7 @@ function workflow_get_roles() {
     $result = db_query('SELECT * FROM {role} ORDER BY name');
     $roles = array('author' => 'author');
     while ($data = db_fetch_object($result)) {
-      $roles[$data->rid] = $data->name;
+      $roles[$data->rid] = filter_xss($data->name);
     }
   }
   return $roles;

Patch for Workflow 5.x-2.3

Applying the following patch mitigates these threats in Workflow 5.x-2.3.

diff -up workflow/workflow_access.module workflow/workflow_access.module
--- workflow/workflow_access.module     2008-07-09 14:27:50.000000000 -0400
+++ workflow/workflow_access.module       2009-10-12 15:12:24.493102986 -0400
@@ -98,7 +98,7 @@ function workflow_access_form_alter($for
   $rids = array('-1' => t('author'));
   $result = db_query("SELECT r.rid, r.name FROM {role} r ORDER BY r.name");
   while ($obj = db_fetch_object($result)) {
-    $rids[$obj->rid] = $obj->name;
+    $rids[$obj->rid] = filter_xss($obj->name);
   }
 
   $form['workflow_access'] = array('#type' => 'fieldset', 
@@ -140,7 +140,7 @@ function workflow_access_form_alter($for
     // TODO better tables using a #theme function instead of direct #prefixing
     $form['workflow_access'][$sid] = array(
       '#type' => 'fieldset', 
-      '#title' => $state,
+      '#title' => filter_xss($state),
       '#collapsible' => TRUE,
       '#tree' => TRUE,
     );
diff -up workflow/workflow.module workflow/workflow.module
--- workflow/workflow.module    2008-07-18 21:45:08.000000000 -0400
+++ workflow/workflow.module      2009-10-12 15:24:03.822097875 -0400
@@ -1329,7 +1329,7 @@ function workflow_transition_grid_form($
           $tid = workflow_get_transition_id($from, $to);
           $form[$from][$to][$rid] = array(
             '#type' => 'checkbox',
-            '#title' => $role_name,
+            '#title' => filter_xss($role_name),
             '#default_value' => $tid ? workflow_transition_allowed($tid, $rid) : FALSE);
         }
       }
@@ -1416,7 +1416,7 @@ function workflow_overview() {
       }
       // Allow modules to insert state operations.
       $state_links = array_merge($state_links, module_invoke_all('workflow_operations', 'state', $wid, $sid));
-      $subrows[] = array(t($state_name), theme('links', $state_links));
+      $subrows[] = array(filter_xss(t($state_name)), theme('links', $state_links));
       unset($state_links);
     }
 
@@ -1574,7 +1574,7 @@ function theme_workflow_types_form($form
   //  if (in_array($form[$key]['#type'], array('select', 'checkboxes'))) {
       $name = $form[$type]['workflow']['#title'];
       unset($form[$type]['workflow']['#title']);
-      $rows[] = array($name, drupal_render($form[$type]['workflow']), drupal_render($form[$type]['placement']));
+      $rows[] = array(filter_xss($name), drupal_render($form[$type]['workflow']), drupal_render($form[$type]['placement']));
   //  }
   }
   $output = drupal_render($form['help']);
@@ -1633,7 +1633,7 @@ function workflow_get_all() {
   $workflows = array();
   $result = db_query("SELECT wid, name FROM {workflows} ORDER BY name ASC");
   while ($data = db_fetch_object($result)) {
-    $workflows[$data->wid] = $data->name;
+    $workflows[$data->wid] = filter_xss($data->name);
   }
 
   return $workflows;
@@ -1715,7 +1715,7 @@ function workflow_get_states($wid = NULL
     $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 1 ORDER BY sid");
   }
   while ($data = db_fetch_object($result)) {
-    $states[$data->sid] = $data->state;
+    $states[$data->sid] = filter_xss($data->state);
   }
 
   return $states;
@@ -1991,7 +1991,7 @@ function workflow_get_roles() {
     $result = db_query('SELECT * FROM {role} ORDER BY name');
     $roles = array('author' => 'author');
     while ($data = db_fetch_object($result)) {
-      $roles[$data->rid] = $data->name;
+      $roles[$data->rid] = filter_xss($data->name);
     }
   }
   return $roles;