Time attack protection on hmac comparison

This fixes issue 2.7 of https://defuse.ca/audits/zerobin.htm, and thus
(with commit a24212afda90ca3e4b4ff5ce30d2012709b58a28) also issue 2.8.

(cherry picked from commit 0b4db7ece313dd268e51fc47a0293a649927558a)

Conflicts:
	index.php
This commit is contained in:
Sebastien SAUVAGE 2014-02-06 22:52:17 +01:00 committed by El RIDO
parent daf5522b1e
commit 43a439e7d0
6 changed files with 135 additions and 58 deletions

View File

@ -50,4 +50,35 @@ class filter
} }
return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . $iec[$i]; return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . $iec[$i];
} }
/**
* validate paste ID
*
* @access public
* @param string $dataid
* @return bool
*/
public static function is_valid_paste_id($dataid)
{
return (bool) preg_match('#\A[a-f\d]{16}\z#', $dataid);
}
/**
* fixed time string comparison operation to prevent timing attacks
* https://crackstation.net/hashing-security.htm?=rd#slowequals
*
* @access public
* @param string $a
* @param string $b
* @return bool
*/
public static function slow_equals($a, $b)
{
$diff = strlen($a) ^ strlen($b);
for($i = 0; $i < strlen($a) && $i < strlen($b); $i++)
{
$diff |= ord($a[$i]) ^ ord($b[$i]);
}
return $diff === 0;
}
} }

View File

@ -272,8 +272,8 @@ class zerobin
$pasteid = $_POST['pasteid']; $pasteid = $_POST['pasteid'];
$parentid = $_POST['parentid']; $parentid = $_POST['parentid'];
if ( if (
!preg_match('#\A[a-f\d]{16}\z#', $pasteid) || !filter::is_valid_paste_id($pasteid) ||
!preg_match('#\A[a-f\d]{16}\z#', $parentid) !filter::is_valid_paste_id($parentid)
) $this->_return_message(1, 'Invalid data.'); ) $this->_return_message(1, 'Invalid data.');
// Comments do not expire (it's the paste that expires) // Comments do not expire (it's the paste that expires)
@ -340,18 +340,21 @@ class zerobin
private function _delete($dataid, $deletetoken) private function _delete($dataid, $deletetoken)
{ {
// Is this a valid paste identifier? // Is this a valid paste identifier?
if (preg_match('#\A[a-f\d]{16}\z#', $dataid)) if (!filter::is_valid_paste_id($dataid))
{ {
$this->_error = 'Invalid paste ID.';
return;
}
// Check that paste exists. // Check that paste exists.
if (!$this->_model()->exists($dataid)) if (!$this->_model()->exists($dataid))
{ {
$this->_error = 'Paste does not exist, has expired or has been deleted.'; $this->_error = 'Paste does not exist, has expired or has been deleted.';
return; return;
} }
}
// Make sure token is valid. // Make sure token is valid.
if ($deletetoken != hash_hmac('sha1', $dataid , serversalt::get())) if (filter::slow_equals($deletetoken, hash_hmac('sha1', $dataid , serversalt::get())))
{ {
$this->_error = 'Wrong deletion token. Paste was not deleted.'; $this->_error = 'Wrong deletion token. Paste was not deleted.';
return; return;
@ -372,8 +375,12 @@ class zerobin
private function _read($dataid) private function _read($dataid)
{ {
// Is this a valid paste identifier? // Is this a valid paste identifier?
if (preg_match('#\A[a-f\d]{16}\z#', $dataid)) if (!filter::is_valid_paste_id($dataid))
{ {
$this->_error = 'Invalid paste ID.';
return;
}
// Check that paste exists. // Check that paste exists.
if ($this->_model()->exists($dataid)) if ($this->_model()->exists($dataid))
{ {
@ -426,11 +433,6 @@ class zerobin
$this->_error = 'Paste does not exist or has expired.'; $this->_error = 'Paste does not exist or has expired.';
} }
} }
else
{
$this->_error = 'Invalid paste ID.';
}
}
/** /**
* Display ZeroBin frontend. * Display ZeroBin frontend.

View File

@ -44,4 +44,25 @@ class filterTest extends PHPUnit_Framework_TestCase
$this->assertEquals('1.00 YiB', filter::size_humanreadable(1024 * $exponent)); $this->assertEquals('1.00 YiB', filter::size_humanreadable(1024 * $exponent));
$this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent)); $this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent));
} }
public function testPasteIdValidation()
{
$this->assertTrue(filter::is_valid_paste_id('a242ab7bdfb2581a'), 'valid paste id');
$this->assertFalse(filter::is_valid_paste_id('foo'), 'invalid hex values');
$this->assertFalse(filter::is_valid_paste_id('../bar/baz'), 'path attack');
}
public function testSlowEquals()
{
$this->assertTrue(filter::slow_equals('foo', 'foo'), 'same string');
$this->assertFalse(filter::slow_equals('foo', true), 'string and boolean');
$this->assertFalse(filter::slow_equals('foo', 0), 'string and integer');
$this->assertFalse(filter::slow_equals('123foo', 123), 'string and integer');
$this->assertFalse(filter::slow_equals('123foo', '123'), 'different strings');
$this->assertFalse(filter::slow_equals('6', ' 6'), 'strings with space');
$this->assertFalse(filter::slow_equals('4.2', '4.20'), 'floats as strings');
$this->assertFalse(filter::slow_equals('1e3', '1000'), 'integers as strings');
$this->assertFalse(filter::slow_equals('9223372036854775807', '9223372036854775808'), 'large integers as strings');
$this->assertFalse(filter::slow_equals('61529519452809720693702583126814', '61529519452809720000000000000000'), 'larger integers as strings');
}
} }

17
tst/mcrypt_mock.php Normal file
View File

@ -0,0 +1,17 @@
<?php
define('MCRYPT_DEV_URANDOM', 1);
function mcrypt_create_iv($int, $flag)
{
$randomSalt = '';
for($i = 0; $i < 16; ++$i) {
$randomSalt .= base_convert(mt_rand(), 10, 16);
}
// hex2bin requires an even length, pad if necessary
if (strlen($randomSalt) % 2)
{
$randomSalt = '0' . $randomSalt;
}
return hex2bin($randomSalt);
}

View File

@ -1,6 +1,7 @@
<phpunit bootstrap="bootstrap.php" colors="true"> <phpunit bootstrap="bootstrap.php" colors="true">
<testsuite name="ZeroBin Test Suite"> <testsuite name="ZeroBin Test Suite">
<directory suffix=".php">./</directory> <directory suffix=".php">./</directory>
<exclude>mcrypt_mock.php</exclude>
</testsuite> </testsuite>
<filter> <filter>
<whitelist> <whitelist>

View File

@ -37,5 +37,10 @@ class vizhash16x16Test extends PHPUnit_Framework_TestCase
$this->assertEquals('image/png', $finfo->file($this->_file)); $this->assertEquals('image/png', $finfo->file($this->_file));
$this->assertNotEquals($pngdata, $vz->generate('2001:1620:2057:dead:beef::cafe:babe')); $this->assertNotEquals($pngdata, $vz->generate('2001:1620:2057:dead:beef::cafe:babe'));
$this->assertEquals($pngdata, $vz->generate('127.0.0.1')); $this->assertEquals($pngdata, $vz->generate('127.0.0.1'));
// generating new salt
$salt = serversalt::get();
require 'mcrypt_mock.php';
$this->assertNotEquals($salt, serversalt::generate());
} }
} }