diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index fa1fd02..ddfdf2c 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,4 +1,8 @@
 
+Drupal 6.29, 2013-11-20
+----------------------
+- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003.
+
 Drupal 6.28, 2013-01-16
 ----------------------
 - Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-001.
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index 3c53a96..d6b407c 100644
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -1334,3 +1334,111 @@ function ip_address() {
 
   return $ip_address;
 }
+
+/**
+ * Returns a URL-safe, base64 encoded string of highly randomized bytes (over the full 8-bit range).
+ *
+ * @param $byte_count
+ *   The number of random bytes to fetch and base64 encode.
+ *
+ * @return string
+ *   The base64 encoded result will have a length of up to 4 * $byte_count.
+ */
+function drupal_random_key($byte_count = 32) {
+  return drupal_base64_encode(drupal_random_bytes($byte_count));
+}
+
+/**
+ * Returns a URL-safe, base64 encoded version of the supplied string.
+ *
+ * @param $string
+ *   The string to convert to base64.
+ *
+ * @return string
+ */
+function drupal_base64_encode($string) {
+  $data = base64_encode($string);
+  // Modify the output so it's safe to use in URLs.
+  return strtr($data, array('+' => '-', '/' => '_', '=' => ''));
+}
+
+/**
+ * Returns a string of highly randomized bytes (over the full 8-bit range).
+ *
+ * This function is better than simply calling mt_rand() or any other built-in
+ * PHP function because it can return a long string of bytes (compared to < 4
+ * bytes normally from mt_rand()) and uses the best available pseudo-random
+ * source.
+ *
+ * @param $count
+ *   The number of characters (bytes) to return in the string.
+ */
+function drupal_random_bytes($count) {
+  // $random_state does not use drupal_static as it stores random bytes.
+  static $random_state, $bytes, $has_openssl, $has_hash;
+
+  $missing_bytes = $count - strlen($bytes);
+
+  if ($missing_bytes > 0) {
+    // PHP versions prior 5.3.4 experienced openssl_random_pseudo_bytes()
+    // locking on Windows and rendered it unusable.
+    if (!isset($has_openssl)) {
+      $has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') && function_exists('openssl_random_pseudo_bytes');
+    }
+
+    // openssl_random_pseudo_bytes() will find entropy in a system-dependent
+    // way.
+    if ($has_openssl) {
+      $bytes .= openssl_random_pseudo_bytes($missing_bytes);
+    }
+
+    // Else, read directly from /dev/urandom, which is available on many *nix
+    // systems and is considered cryptographically secure.
+    elseif ($fh = @fopen('/dev/urandom', 'rb')) {
+      // PHP only performs buffered reads, so in reality it will always read
+      // at least 4096 bytes. Thus, it costs nothing extra to read and store
+      // that much so as to speed any additional invocations.
+      $bytes .= fread($fh, max(4096, $missing_bytes));
+      fclose($fh);
+    }
+
+    // If we couldn't get enough entropy, this simple hash-based PRNG will
+    // generate a good set of pseudo-random bytes on any system.
+    // Note that it may be important that our $random_state is passed
+    // through hash() prior to being rolled into $output, that the two hash()
+    // invocations are different, and that the extra input into the first one -
+    // the microtime() - is prepended rather than appended. This is to avoid
+    // directly leaking $random_state via the $output stream, which could
+    // allow for trivial prediction of further "random" numbers.
+    if (strlen($bytes) < $count) {
+      // Initialize on the first call. The contents of $_SERVER includes a mix of
+      // user-specific and system information that varies a little with each page.
+      if (!isset($random_state)) {
+        $random_state = print_r($_SERVER, TRUE);
+        if (function_exists('getmypid')) {
+          // Further initialize with the somewhat random PHP process ID.
+          $random_state .= getmypid();
+        }
+        // hash() is only available in PHP 5.1.2+ or via PECL.
+        $has_hash = function_exists('hash') && in_array('sha256', hash_algos());
+        $bytes = '';
+      }
+
+      if ($has_hash) {
+        do {
+          $random_state = hash('sha256', microtime() . mt_rand() . $random_state);
+          $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
+        } while (strlen($bytes) < $count);
+      }
+      else {
+        do {
+          $random_state = md5(microtime() . mt_rand() . $random_state);
+          $bytes .= pack("H*", md5(mt_rand() . $random_state));
+        } while (strlen($bytes) < $count);
+      }
+    }
+  }
+  $output = substr($bytes, 0, $count);
+  $bytes = substr($bytes, $count);
+  return $output;
+}
diff --git a/includes/common.inc b/includes/common.inc
index 2abf198..27ed5cd 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2634,7 +2634,7 @@ function drupal_urlencode($text) {
  */
 function drupal_get_private_key() {
   if (!($key = variable_get('drupal_private_key', 0))) {
-    $key = md5(uniqid(mt_rand(), true)) . md5(uniqid(mt_rand(), true));
+    $key = drupal_random_key();
     variable_set('drupal_private_key', $key);
   }
   return $key;
@@ -2666,7 +2666,7 @@ function drupal_get_token($value = '') {
  */
 function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) {
   global $user;
-  return (($skip_anonymous && $user->uid == 0) || ($token == md5(session_id() . $value . variable_get('drupal_private_key', ''))));
+  return (($skip_anonymous && $user->uid == 0) || ($token === md5(session_id() . $value . variable_get('drupal_private_key', ''))));
 }
 
 /**
@@ -2724,6 +2724,10 @@ function _drupal_bootstrap_full() {
   fix_gpc_magic();
   // Load all enabled modules
   module_load_all();
+  // Ensure mt_rand is reseeded, to prevent random values from one page load
+  // being exploited to predict random values in subsequent page loads.
+  $seed = unpack("L", drupal_random_bytes(4));
+  mt_srand($seed[1]);
   // Let all modules take action before menu system handles the request
   // We do not want this while running update.php.
   if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
diff --git a/includes/file.inc b/includes/file.inc
index 0736f8b..d0e24b2 100644
--- a/includes/file.inc
+++ b/includes/file.inc
@@ -134,20 +134,81 @@ function file_check_directory(&$directory, $mode = 0, $form_item = NULL) {
     }
   }
 
-  if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) {
-    $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks";
+  if (file_directory_path() == $directory || file_directory_temp() == $directory) {
+    file_create_htaccess($directory, $form_item);
+  }
+
+  return TRUE;
+}
+
+/**
+ * Creates a .htaccess file in the given directory.
+ *
+ * @param $directory
+ *   The directory.
+ * @param $form_item
+ *   An optional string containing the name of a form item that any errors
+ *   will be attached to. Useful when called from file_check_directory() to
+ *   validate a directory path entered as a form value. An error will
+ *   consequently prevent form submit handlers from running, and instead
+ *   display the form along with the error messages.
+ * @param $force_overwrite
+ *   Set to TRUE to attempt to overwrite the existing .htaccess file if one is
+ *   already present. Defaults to FALSE.
+ */
+function file_create_htaccess($directory, $form_item = NULL, $force_overwrite = FALSE) {
+  if (!is_file("$directory/.htaccess") || $force_overwrite) {
+    $htaccess_lines = file_htaccess_lines();
     if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) {
       fclose($fp);
       chmod($directory .'/.htaccess', 0664);
     }
     else {
       $variables = array('%directory' => $directory, '!htaccess' => '<br />'. nl2br(check_plain($htaccess_lines)));
-      form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables));
+      if ($form_item) {
+        form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables));
+      }
       watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines: <code>!htaccess</code>", $variables, WATCHDOG_ERROR);
     }
   }
+}
 
-  return TRUE;
+/**
+ * Returns the standard .htaccess lines that Drupal writes to file directories.
+ *
+ * @return
+ *   A string representing the desired contents of the .htaccess file.
+ *
+ * @see file_create_htaccess()
+ */
+function file_htaccess_lines() {
+  $lines = <<<EOF
+# Turn off all options we don't need.
+Options None
+Options +FollowSymLinks
+
+# Set the catch-all handler to prevent scripts from being executed.
+SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
+<Files *>
+  # Override the handler again if we're run later in the evaluation list.
+  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
+</Files>
+
+# If we know how to do it safely, disable the PHP engine entirely.
+<IfModule mod_php5.c>
+  php_flag engine off
+</IfModule>
+# PHP 4, Apache 1.
+<IfModule mod_php4.c>
+  php_flag engine off
+</IfModule>
+# PHP 4, Apache 2.
+<IfModule sapi_apache2.c>
+  php_flag engine off
+</IfModule>
+EOF;
+
+  return $lines;
 }
 
 /**
diff --git a/includes/form.inc b/includes/form.inc
index 514641d..1b0e057 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -101,7 +101,7 @@ function drupal_get_form($form_id) {
       array_unshift($args_temp, $form_id);
 
       $form = call_user_func_array('drupal_retrieve_form', $args_temp);
-      $form_build_id = 'form-'. md5(uniqid(mt_rand(), TRUE));
+      $form_build_id = 'form-'. drupal_random_key();
       $form['#build_id'] = $form_build_id;
       drupal_prepare_form($form_id, $form, $form_state);
       // Store a copy of the unprocessed form for caching and indicate that it
@@ -196,7 +196,7 @@ function drupal_rebuild_form($form_id, &$form_state, $args, $form_build_id = NUL
 
   if (!isset($form_build_id)) {
     // We need a new build_id for the new version of the form.
-    $form_build_id = 'form-'. md5(uniqid(mt_rand(), TRUE));
+    $form_build_id = 'form-'. drupal_random_key();
   }
   $form['#build_id'] = $form_build_id;
   drupal_prepare_form($form_id, $form, $form_state);
@@ -590,6 +590,12 @@ function drupal_validate_form($form_id, $form, &$form_state) {
     if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) {
       // Setting this error will cause the form to fail validation.
       form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.'));
+
+      // Stop here and don't run any further validation handlers, because they
+      // could invoke non-safe operations which opens the door for CSRF
+      // vulnerabilities.
+      $validated_forms[$form_id] = TRUE;
+      return;
     }
   }
 
diff --git a/install.php b/install.php
index 0773d33..0839d38 100644
--- a/install.php
+++ b/install.php
@@ -139,6 +139,16 @@ function install_main() {
 
     // Install system.module.
     drupal_install_system();
+
+    // Ensure that all of Drupal's standard directories have appropriate
+    // .htaccess files. These directories will have already been created by
+    // this point in the installer, since Drupal creates them during the
+    // install_check_requirements() task. Note that we cannot create them any
+    // earlier than this, since the code below relies on system.module in order
+    // to work.
+    file_create_htaccess(file_directory_path());
+    file_create_htaccess(file_directory_temp());
+
     // Save the list of other modules to install for the 'profile-install'
     // task. variable_set() can be used now that system.module is installed
     // and drupal is bootstrapped.
diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc
index 44cdde2..70dbee9 100644
--- a/modules/openid/openid.inc
+++ b/modules/openid/openid.inc
@@ -361,7 +361,7 @@ function _openid_dh_rand($stop) {
   }
 
   do {
-    $bytes = "\x00". _openid_get_bytes($nbytes);
+    $bytes = "\x00". drupal_random_bytes($nbytes);
     $n = _openid_dh_binary_to_long($bytes);
     // Keep looping if this value is in the low duplicated range.
   } while (bccomp($n, $duplicate) < 0);
@@ -370,23 +370,7 @@ function _openid_dh_rand($stop) {
 }
 
 function _openid_get_bytes($num_bytes) {
-  static $f = null;
-  $bytes = '';
-  if (!isset($f)) {
-    $f = @fopen(OPENID_RAND_SOURCE, "r");
-  }
-  if (!$f) {
-    // pseudorandom used
-    $bytes = '';
-    for ($i = 0; $i < $num_bytes; $i += 4) {
-      $bytes .= pack('L', mt_rand());
-    }
-    $bytes = substr($bytes, 0, $num_bytes);
-  }
-  else {
-    $bytes = fread($f, $num_bytes);
-  }
-  return $bytes;
+  return drupal_random_bytes($num_bytes);
 }
 
 function _openid_response($str = NULL) {
diff --git a/modules/system/system.install b/modules/system/system.install
index 2421dcb..acacd39 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -122,6 +122,35 @@ function system_requirements($phase) {
     $requirements['settings.php']['title'] = $t('Configuration file');
   }
 
+  // Test the contents of the .htaccess files.
+  if ($phase == 'runtime') {
+    // Try to write the .htaccess files first, to prevent false alarms in case
+    // (for example) the /tmp directory was wiped.
+    file_create_htaccess(file_directory_path());
+    file_create_htaccess(file_directory_temp());
+    $htaccess_files['files_htaccess'] = array(
+      'title' => $t('Files directory'),
+      'directory' => file_directory_path(),
+    );
+    $htaccess_files['temporary_files_htaccess'] = array(
+      'title' => $t('Temporary files directory'),
+      'directory' => file_directory_temp(),
+    );
+    foreach ($htaccess_files as $key => $file_info) {
+      // Check for the string which was added to the recommended .htaccess file
+      // in the latest security update.
+      $htaccess_file = $file_info['directory'] . '/.htaccess';
+      if (!file_exists($htaccess_file) || !($contents = @file_get_contents($htaccess_file)) || strpos($contents, 'Drupal_Security_Do_Not_Remove_See_SA_2013_003') === FALSE) {
+        $requirements[$key] = array(
+          'title' => $file_info['title'],
+          'value' => $t('Not fully protected'),
+          'severity' => REQUIREMENT_ERROR,
+          'description' => $t('See <a href="@url">@url</a> for information about the recommended .htaccess file which should be added to the %directory directory to help protect against arbitrary code execution.', array('@url' => 'http://drupal.org/SA-CORE-2013-003', '%directory' => $file_info['directory'])),
+        );
+      }
+    }
+  }
+
   // Report cron status.
   if ($phase == 'runtime') {
     // Cron warning threshold defaults to two days.
diff --git a/modules/system/system.module b/modules/system/system.module
index 320f51f..09732c9 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -8,7 +8,7 @@
 /**
  * The current system version.
  */
-define('VERSION', '6.28');
+define('VERSION', '6.29');
 
 /**
  * Core API compatibility.
diff --git a/modules/user/user.module b/modules/user/user.module
index 9339c44..0dea21e 100644
--- a/modules/user/user.module
+++ b/modules/user/user.module
@@ -477,12 +477,15 @@ function user_password($length = 10) {
 
   // Loop the number of times specified by $length.
   for ($i = 0; $i < $length; $i++) {
+    do {
+      // Find a secure random number within the range needed.
+      $index = ord(drupal_random_bytes(1));
+    } while ($index > $len);
 
     // Each iteration, pick a random character from the
     // allowable string and append it to the password:
-    $pass .= $allowable_characters[mt_rand(0, $len)];
+    $pass .= $allowable_characters[$index];
   }
-
   return $pass;
 }
 
diff --git a/update.php b/update.php
index 9840657..0d62ae7 100644
--- a/update.php
+++ b/update.php
@@ -655,13 +655,13 @@ if (!empty($update_free_access) || $user->uid == 1) {
   $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
   switch ($op) {
     case 'selection':
-      if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) {
+      if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'update')) {
         $output = update_selection_page();
         break;
       }
 
     case 'Update':
-      if (isset($_GET['token']) && $_GET['token'] == drupal_get_token('update')) {
+      if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'update')) {
         update_batch();
         break;
       }
