diff --git a/extlib/Auth/OpenID/Association.php b/extlib/Auth/OpenID/Association.php index 7fdf399a3c..2729138ebb 100644 --- a/extlib/Auth/OpenID/Association.php +++ b/extlib/Auth/OpenID/Association.php @@ -374,42 +374,7 @@ class Auth_OpenID_Association { } $calculated_sig = $this->getMessageSignature($message); - - return $this->constantTimeCompare($calculated_sig, $sig); - } - - /** - * String comparison function which will complete in a constant time - * for strings of any given matching length, to help prevent an attacker - * from distinguishing how much of a signature token they have guessed - * correctly. - * - * For this usage, it's assumed that the length of the string is known, - * so we may safely short-circuit on mismatched lengths which will be known - * to be invalid by the attacker. - * - * http://lists.openid.net/pipermail/openid-security/2010-July/001156.html - * http://rdist.root.org/2010/01/07/timing-independent-array-comparison/ - */ - private function constantTimeCompare($a, $b) - { - $len = strlen($a); - if (strlen($b) !== $len) { - // Short-circuit on length mismatch; attackers will already know - // the correct target length so this is safe. - return false; - } - if ($len == 0) { - // 0-length valid input shouldn't really happen. :) - return true; - } - $result = 0; - for ($i = 0; $i < strlen($a); $i++) { - // We use scary bitwise operations to avoid logical short-circuits - // in lower-level code. - $result |= ord($a{$i}) ^ ord($b{$i}); - } - return ($result == 0); + return Auth_OpenID_CryptUtil::constEq($calculated_sig, $sig); } } diff --git a/extlib/Auth/OpenID/BigMath.php b/extlib/Auth/OpenID/BigMath.php index 7fca2dc43e..58b46bf27b 100644 --- a/extlib/Auth/OpenID/BigMath.php +++ b/extlib/Auth/OpenID/BigMath.php @@ -365,7 +365,6 @@ function Auth_OpenID_detectMathLibrary($exts) { $loaded = false; - $hasDl = function_exists('dl'); foreach ($exts as $extension) { if (extension_loaded($extension['extension'])) { return $extension; diff --git a/extlib/Auth/OpenID/Consumer.php b/extlib/Auth/OpenID/Consumer.php index 021c038988..d562e33f35 100644 --- a/extlib/Auth/OpenID/Consumer.php +++ b/extlib/Auth/OpenID/Consumer.php @@ -957,6 +957,10 @@ class Auth_OpenID_GenericConsumer { } if (!$assoc->checkMessageSignature($message)) { + // If we get a "bad signature" here, it means that the association + // is unrecoverabley corrupted in some way. Any futher attempts + // to login with this association is likely to fail. Drop it. + $this->store->removeAssociation($server_url, $assoc_handle); return new Auth_OpenID_FailureResponse(null, "Bad signature"); } @@ -1179,9 +1183,11 @@ class Auth_OpenID_GenericConsumer { function _discoverAndVerify($claimed_id, $to_match_endpoints) { // oidutil.log('Performing discovery on %s' % (claimed_id,)) - list($unused, $services) = call_user_func($this->discoverMethod, - $claimed_id, - &$this->fetcher); + list($unused, $services) = call_user_func_array($this->discoverMethod, + array( + $claimed_id, + &$this->fetcher, + )); if (!$services) { return new Auth_OpenID_FailureResponse(null, diff --git a/extlib/Auth/OpenID/CryptUtil.php b/extlib/Auth/OpenID/CryptUtil.php index a926267779..3c60cea170 100644 --- a/extlib/Auth/OpenID/CryptUtil.php +++ b/extlib/Auth/OpenID/CryptUtil.php @@ -104,5 +104,19 @@ class Auth_OpenID_CryptUtil { return $str; } + + static function constEq($s1, $s2) + { + if (strlen($s1) != strlen($s2)) { + return false; + } + + $result = true; + $length = strlen($s1); + for ($i = 0; $i < $length; $i++) { + $result &= ($s1[$i] == $s2[$i]); + } + return $result; + } } diff --git a/extlib/Auth/OpenID/Extension.php b/extlib/Auth/OpenID/Extension.php index c4e38c0380..542a1da261 100644 --- a/extlib/Auth/OpenID/Extension.php +++ b/extlib/Auth/OpenID/Extension.php @@ -39,7 +39,7 @@ class Auth_OpenID_Extension { * * Returns the message with the extension arguments added. */ - function toMessage($message) + function toMessage($message, $request = null) { $implicit = $message->isOpenID1(); $added = $message->namespaces->addAlias($this->ns_uri, @@ -53,8 +53,13 @@ class Auth_OpenID_Extension { } } - $message->updateArgs($this->ns_uri, - $this->getExtensionArgs()); + if ($request !== null) { + $message->updateArgs($this->ns_uri, + $this->getExtensionArgs($request)); + } else { + $message->updateArgs($this->ns_uri, + $this->getExtensionArgs()); + } return $message; } } diff --git a/extlib/Auth/OpenID/FileStore.php b/extlib/Auth/OpenID/FileStore.php index 074421a0bb..7eec791d24 100644 --- a/extlib/Auth/OpenID/FileStore.php +++ b/extlib/Auth/OpenID/FileStore.php @@ -300,13 +300,22 @@ class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { return null; } + if (file_exists($filename) !== true) { + return null; + } + $assoc_file = @fopen($filename, 'rb'); if ($assoc_file === false) { return null; } - $assoc_s = fread($assoc_file, filesize($filename)); + $filesize = filesize($filename); + if ($filesize === false || $filesize <= 0) { + return null; + } + + $assoc_s = fread($assoc_file, $filesize); fclose($assoc_file); if (!$assoc_s) { diff --git a/extlib/Auth/OpenID/HMAC.php b/extlib/Auth/OpenID/HMAC.php index e9779bd4e0..e6c4bdfd9d 100644 --- a/extlib/Auth/OpenID/HMAC.php +++ b/extlib/Auth/OpenID/HMAC.php @@ -60,6 +60,13 @@ function Auth_OpenID_HMACSHA1($key, $text) $key = Auth_OpenID_SHA1($key, true); } + if (function_exists('hash_hmac') && + function_exists('hash_algos') && + (in_array('sha1', hash_algos()))) { + return hash_hmac('sha1', $text, $key, true); + } + // Home-made solution + $key = str_pad($key, Auth_OpenID_SHA1_BLOCKSIZE, chr(0x00)); $ipad = str_repeat(chr(0x36), Auth_OpenID_SHA1_BLOCKSIZE); $opad = str_repeat(chr(0x5c), Auth_OpenID_SHA1_BLOCKSIZE); diff --git a/extlib/Auth/OpenID/MDB2Store.php b/extlib/Auth/OpenID/MDB2Store.php new file mode 100644 index 0000000000..fb27d5c4d5 --- /dev/null +++ b/extlib/Auth/OpenID/MDB2Store.php @@ -0,0 +1,413 @@ + + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +require_once 'MDB2.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/Interface.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/Nonce.php'; + +/** + * This store uses a PEAR::MDB2 connection to store persistence + * information. + * + * The table names used are determined by the class variables + * associations_table_name and nonces_table_name. To change the name + * of the tables used, pass new table names into the constructor. + * + * To create the tables with the proper schema, see the createTables + * method. + * + * @package OpenID + */ +class Auth_OpenID_MDB2Store extends Auth_OpenID_OpenIDStore { + /** + * This creates a new MDB2Store instance. It requires an + * established database connection be given to it, and it allows + * overriding the default table names. + * + * @param connection $connection This must be an established + * connection to a database of the correct type for the SQLStore + * subclass you're using. This must be a PEAR::MDB2 connection + * handle. + * + * @param associations_table: This is an optional parameter to + * specify the name of the table used for storing associations. + * The default value is 'oid_associations'. + * + * @param nonces_table: This is an optional parameter to specify + * the name of the table used for storing nonces. The default + * value is 'oid_nonces'. + */ + function Auth_OpenID_MDB2Store($connection, + $associations_table = null, + $nonces_table = null) + { + $this->associations_table_name = "oid_associations"; + $this->nonces_table_name = "oid_nonces"; + + // Check the connection object type to be sure it's a PEAR + // database connection. + if (!is_object($connection) || + !is_subclass_of($connection, 'mdb2_driver_common')) { + trigger_error("Auth_OpenID_MDB2Store expected PEAR connection " . + "object (got ".get_class($connection).")", + E_USER_ERROR); + return; + } + + $this->connection = $connection; + + // Be sure to set the fetch mode so the results are keyed on + // column name instead of column index. + $this->connection->setFetchMode(MDB2_FETCHMODE_ASSOC); + + if (@PEAR::isError($this->connection->loadModule('Extended'))) { + trigger_error("Unable to load MDB2_Extended module", E_USER_ERROR); + return; + } + + if ($associations_table) { + $this->associations_table_name = $associations_table; + } + + if ($nonces_table) { + $this->nonces_table_name = $nonces_table; + } + + $this->max_nonce_age = 6 * 60 * 60; + } + + function tableExists($table_name) + { + return !@PEAR::isError($this->connection->query( + sprintf("SELECT * FROM %s LIMIT 0", + $table_name))); + } + + function createTables() + { + $n = $this->create_nonce_table(); + $a = $this->create_assoc_table(); + + if (!$n || !$a) { + return false; + } + return true; + } + + function create_nonce_table() + { + if (!$this->tableExists($this->nonces_table_name)) { + switch ($this->connection->phptype) { + case "mysql": + case "mysqli": + // Custom SQL for MySQL to use InnoDB and variable- + // length keys + $r = $this->connection->exec( + sprintf("CREATE TABLE %s (\n". + " server_url VARCHAR(2047) NOT NULL DEFAULT '',\n". + " timestamp INTEGER NOT NULL,\n". + " salt CHAR(40) NOT NULL,\n". + " UNIQUE (server_url(255), timestamp, salt)\n". + ") TYPE=InnoDB", + $this->nonces_table_name)); + if (@PEAR::isError($r)) { + return false; + } + break; + default: + if (@PEAR::isError( + $this->connection->loadModule('Manager'))) { + return false; + } + $fields = array( + "server_url" => array( + "type" => "text", + "length" => 2047, + "notnull" => true + ), + "timestamp" => array( + "type" => "integer", + "notnull" => true + ), + "salt" => array( + "type" => "text", + "length" => 40, + "fixed" => true, + "notnull" => true + ) + ); + $constraint = array( + "unique" => 1, + "fields" => array( + "server_url" => true, + "timestamp" => true, + "salt" => true + ) + ); + + $r = $this->connection->createTable($this->nonces_table_name, + $fields); + if (@PEAR::isError($r)) { + return false; + } + + $r = $this->connection->createConstraint( + $this->nonces_table_name, + $this->nonces_table_name . "_constraint", + $constraint); + if (@PEAR::isError($r)) { + return false; + } + break; + } + } + return true; + } + + function create_assoc_table() + { + if (!$this->tableExists($this->associations_table_name)) { + switch ($this->connection->phptype) { + case "mysql": + case "mysqli": + // Custom SQL for MySQL to use InnoDB and variable- + // length keys + $r = $this->connection->exec( + sprintf("CREATE TABLE %s(\n". + " server_url VARCHAR(2047) NOT NULL DEFAULT '',\n". + " handle VARCHAR(255) NOT NULL,\n". + " secret BLOB NOT NULL,\n". + " issued INTEGER NOT NULL,\n". + " lifetime INTEGER NOT NULL,\n". + " assoc_type VARCHAR(64) NOT NULL,\n". + " PRIMARY KEY (server_url(255), handle)\n". + ") TYPE=InnoDB", + $this->associations_table_name)); + if (@PEAR::isError($r)) { + return false; + } + break; + default: + if (@PEAR::isError( + $this->connection->loadModule('Manager'))) { + return false; + } + $fields = array( + "server_url" => array( + "type" => "text", + "length" => 2047, + "notnull" => true + ), + "handle" => array( + "type" => "text", + "length" => 255, + "notnull" => true + ), + "secret" => array( + "type" => "blob", + "length" => "255", + "notnull" => true + ), + "issued" => array( + "type" => "integer", + "notnull" => true + ), + "lifetime" => array( + "type" => "integer", + "notnull" => true + ), + "assoc_type" => array( + "type" => "text", + "length" => 64, + "notnull" => true + ) + ); + $options = array( + "primary" => array( + "server_url" => true, + "handle" => true + ) + ); + + $r = $this->connection->createTable( + $this->associations_table_name, + $fields, + $options); + if (@PEAR::isError($r)) { + return false; + } + break; + } + } + return true; + } + + function storeAssociation($server_url, $association) + { + $fields = array( + "server_url" => array( + "value" => $server_url, + "key" => true + ), + "handle" => array( + "value" => $association->handle, + "key" => true + ), + "secret" => array( + "value" => $association->secret, + "type" => "blob" + ), + "issued" => array( + "value" => $association->issued + ), + "lifetime" => array( + "value" => $association->lifetime + ), + "assoc_type" => array( + "value" => $association->assoc_type + ) + ); + + return !@PEAR::isError($this->connection->replace( + $this->associations_table_name, + $fields)); + } + + function cleanupNonces() + { + global $Auth_OpenID_SKEW; + $v = time() - $Auth_OpenID_SKEW; + + return $this->connection->exec( + sprintf("DELETE FROM %s WHERE timestamp < %d", + $this->nonces_table_name, $v)); + } + + function cleanupAssociations() + { + return $this->connection->exec( + sprintf("DELETE FROM %s WHERE issued + lifetime < %d", + $this->associations_table_name, time())); + } + + function getAssociation($server_url, $handle = null) + { + $sql = ""; + $params = null; + $types = array( + "text", + "blob", + "integer", + "integer", + "text" + ); + if ($handle !== null) { + $sql = sprintf("SELECT handle, secret, issued, lifetime, assoc_type " . + "FROM %s WHERE server_url = ? AND handle = ?", + $this->associations_table_name); + $params = array($server_url, $handle); + } else { + $sql = sprintf("SELECT handle, secret, issued, lifetime, assoc_type " . + "FROM %s WHERE server_url = ? ORDER BY issued DESC", + $this->associations_table_name); + $params = array($server_url); + } + + $assoc = $this->connection->getRow($sql, $types, $params); + + if (!$assoc || @PEAR::isError($assoc)) { + return null; + } else { + $association = new Auth_OpenID_Association($assoc['handle'], + stream_get_contents( + $assoc['secret']), + $assoc['issued'], + $assoc['lifetime'], + $assoc['assoc_type']); + fclose($assoc['secret']); + return $association; + } + } + + function removeAssociation($server_url, $handle) + { + $r = $this->connection->execParam( + sprintf("DELETE FROM %s WHERE server_url = ? AND handle = ?", + $this->associations_table_name), + array($server_url, $handle)); + + if (@PEAR::isError($r) || $r == 0) { + return false; + } + return true; + } + + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + if (abs($timestamp - time()) > $Auth_OpenID_SKEW ) { + return false; + } + + $fields = array( + "timestamp" => $timestamp, + "salt" => $salt + ); + + if (!empty($server_url)) { + $fields["server_url"] = $server_url; + } + + $r = $this->connection->autoExecute( + $this->nonces_table_name, + $fields, + MDB2_AUTOQUERY_INSERT); + + if (@PEAR::isError($r)) { + return false; + } + return true; + } + + /** + * Resets the store by removing all records from the store's + * tables. + */ + function reset() + { + $this->connection->query(sprintf("DELETE FROM %s", + $this->associations_table_name)); + + $this->connection->query(sprintf("DELETE FROM %s", + $this->nonces_table_name)); + } + +} + +?> diff --git a/extlib/Auth/OpenID/Message.php b/extlib/Auth/OpenID/Message.php index 9aa1fa4684..16ec1c1db4 100644 --- a/extlib/Auth/OpenID/Message.php +++ b/extlib/Auth/OpenID/Message.php @@ -675,7 +675,7 @@ class Auth_OpenID_Message { if ($form_tag_attrs) { foreach ($form_tag_attrs as $name => $attr) { - $form .= sprintf(" %s=\"%s\"", $name, $attr); + $form .= sprintf(" %s=\"%s\"", $name, htmlspecialchars($attr)); } } @@ -684,11 +684,11 @@ class Auth_OpenID_Message { foreach ($this->toPostArgs() as $name => $value) { $form .= sprintf( "\n", - $name, $value); + htmlspecialchars($name), htmlspecialchars($value)); } $form .= sprintf("\n", - $submit_text); + htmlspecialchars($submit_text)); $form .= "\n"; diff --git a/extlib/Auth/OpenID/MySQLStore.php b/extlib/Auth/OpenID/MySQLStore.php index 810f059f1d..a5299b3a5c 100644 --- a/extlib/Auth/OpenID/MySQLStore.php +++ b/extlib/Auth/OpenID/MySQLStore.php @@ -32,7 +32,7 @@ class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore { $this->sql['assoc_table'] = "CREATE TABLE %s (\n". - " server_url BLOB NOT NULL,\n". + " server_url VARCHAR(2047) NOT NULL,\n". " handle VARCHAR(255) NOT NULL,\n". " secret BLOB NOT NULL,\n". " issued INTEGER NOT NULL,\n". diff --git a/extlib/Auth/OpenID/Parse.php b/extlib/Auth/OpenID/Parse.php index 6c2e721691..0461bdcff7 100644 --- a/extlib/Auth/OpenID/Parse.php +++ b/extlib/Auth/OpenID/Parse.php @@ -219,7 +219,11 @@ class Auth_OpenID_Parse { function match($regexp, $text, &$match) { if (!is_callable('mb_ereg_search_init')) { - return preg_match($regexp, $text, $match); + if (!preg_match($regexp, $text, $match)) { + return false; + } + $match = $match[0]; + return true; } $regexp = substr($regexp, 1, strlen($regexp) - 2 - strlen($this->_re_flags)); @@ -227,7 +231,7 @@ class Auth_OpenID_Parse { if (!mb_ereg_search($regexp)) { return false; } - list($match) = mb_ereg_search_getregs(); + $match = mb_ereg_search_getregs(); return true; } @@ -269,7 +273,7 @@ class Auth_OpenID_Parse { // Try to find the
tag. $head_re = $this->headFind(); - $head_match = ''; + $head_match = array(); if (!$this->match($head_re, $stripped, $head_match)) { ini_set( 'pcre.backtrack_limit', $old_btlimit ); return array(); @@ -278,7 +282,7 @@ class Auth_OpenID_Parse { $link_data = array(); $link_matches = array(); - if (!preg_match_all($this->_link_find, $head_match, + if (!preg_match_all($this->_link_find, $head_match[0], $link_matches)) { ini_set( 'pcre.backtrack_limit', $old_btlimit ); return array(); diff --git a/extlib/Auth/OpenID/PredisStore.php b/extlib/Auth/OpenID/PredisStore.php new file mode 100644 index 0000000000..7108c2faf9 --- /dev/null +++ b/extlib/Auth/OpenID/PredisStore.php @@ -0,0 +1,208 @@ += 5.3. + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author Ville Mattila