diff --git a/lib/filter.php b/lib/filter.php
index 623b8bb..2face50 100644
--- a/lib/filter.php
+++ b/lib/filter.php
@@ -50,4 +50,35 @@ class filter
}
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;
+ }
}
diff --git a/lib/zerobin.php b/lib/zerobin.php
index 0fb350b..e5ca81c 100644
--- a/lib/zerobin.php
+++ b/lib/zerobin.php
@@ -272,8 +272,8 @@ class zerobin
$pasteid = $_POST['pasteid'];
$parentid = $_POST['parentid'];
if (
- !preg_match('#\A[a-f\d]{16}\z#', $pasteid) ||
- !preg_match('#\A[a-f\d]{16}\z#', $parentid)
+ !filter::is_valid_paste_id($pasteid) ||
+ !filter::is_valid_paste_id($parentid)
) $this->_return_message(1, 'Invalid data.');
// Comments do not expire (it's the paste that expires)
@@ -340,18 +340,21 @@ class zerobin
private function _delete($dataid, $deletetoken)
{
// Is this a valid paste identifier?
- if (preg_match('#\A[a-f\d]{16}\z#', $dataid))
+ if (!filter::is_valid_paste_id($dataid))
{
- // Check that paste exists.
- if (!$this->_model()->exists($dataid))
- {
- $this->_error = 'Paste does not exist, has expired or has been deleted.';
- return;
- }
+ $this->_error = 'Invalid paste ID.';
+ return;
+ }
+
+ // Check that paste exists.
+ if (!$this->_model()->exists($dataid))
+ {
+ $this->_error = 'Paste does not exist, has expired or has been deleted.';
+ return;
}
// 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.';
return;
@@ -372,63 +375,62 @@ class zerobin
private function _read($dataid)
{
// Is this a valid paste identifier?
- if (preg_match('#\A[a-f\d]{16}\z#', $dataid))
+ if (!filter::is_valid_paste_id($dataid))
{
- // Check that paste exists.
- if ($this->_model()->exists($dataid))
+ $this->_error = 'Invalid paste ID.';
+ return;
+ }
+
+ // Check that paste exists.
+ if ($this->_model()->exists($dataid))
+ {
+ // Get the paste itself.
+ $paste = $this->_model()->read($dataid);
+
+ // See if paste has expired.
+ if (
+ isset($paste->meta->expire_date) &&
+ $paste->meta->expire_date < time()
+ )
{
- // Get the paste itself.
- $paste = $this->_model()->read($dataid);
-
- // See if paste has expired.
- if (
- isset($paste->meta->expire_date) &&
- $paste->meta->expire_date < time()
- )
- {
- // Delete the paste
- $this->_model()->delete($dataid);
- $this->_error = 'Paste does not exist, has expired or has been deleted.';
- }
- // If no error, return the paste.
- else
- {
- // We kindly provide the remaining time before expiration (in seconds)
- if (
- property_exists($paste->meta, 'expire_date')
- ) $paste->meta->remaining_time = $paste->meta->expire_date - time();
-
- // The paste itself is the first in the list of encrypted messages.
- $messages = array($paste);
-
- // If it's a discussion, get all comments.
- if (
- property_exists($paste->meta, 'opendiscussion') &&
- $paste->meta->opendiscussion
- )
- {
- $messages = array_merge(
- $messages,
- $this->_model()->readComments($dataid)
- );
- }
- $this->_data = json_encode($messages);
-
- // If the paste was meant to be read only once, delete it.
- if (
- property_exists($paste->meta, 'burnafterreading') &&
- $paste->meta->burnafterreading
- ) $this->_model()->delete($dataid);
- }
+ // Delete the paste
+ $this->_model()->delete($dataid);
+ $this->_error = 'Paste does not exist, has expired or has been deleted.';
}
+ // If no error, return the paste.
else
{
- $this->_error = 'Paste does not exist or has expired.';
+ // We kindly provide the remaining time before expiration (in seconds)
+ if (
+ property_exists($paste->meta, 'expire_date')
+ ) $paste->meta->remaining_time = $paste->meta->expire_date - time();
+
+ // The paste itself is the first in the list of encrypted messages.
+ $messages = array($paste);
+
+ // If it's a discussion, get all comments.
+ if (
+ property_exists($paste->meta, 'opendiscussion') &&
+ $paste->meta->opendiscussion
+ )
+ {
+ $messages = array_merge(
+ $messages,
+ $this->_model()->readComments($dataid)
+ );
+ }
+ $this->_data = json_encode($messages);
+
+ // If the paste was meant to be read only once, delete it.
+ if (
+ property_exists($paste->meta, 'burnafterreading') &&
+ $paste->meta->burnafterreading
+ ) $this->_model()->delete($dataid);
}
}
else
{
- $this->_error = 'Invalid paste ID.';
+ $this->_error = 'Paste does not exist or has expired.';
}
}
diff --git a/tst/filter.php b/tst/filter.php
index c8d4239..c058a66 100644
--- a/tst/filter.php
+++ b/tst/filter.php
@@ -44,4 +44,25 @@ class filterTest extends PHPUnit_Framework_TestCase
$this->assertEquals('1.00 YiB', filter::size_humanreadable(1024 * $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');
+ }
}
diff --git a/tst/mcrypt_mock.php b/tst/mcrypt_mock.php
new file mode 100644
index 0000000..96b0ed3
--- /dev/null
+++ b/tst/mcrypt_mock.php
@@ -0,0 +1,17 @@
+
./
+ mcrypt_mock.php
diff --git a/tst/vizhash16x16.php b/tst/vizhash16x16.php
index 82f06a0..b33d48d 100644
--- a/tst/vizhash16x16.php
+++ b/tst/vizhash16x16.php
@@ -37,5 +37,10 @@ class vizhash16x16Test extends PHPUnit_Framework_TestCase
$this->assertEquals('image/png', $finfo->file($this->_file));
$this->assertNotEquals($pngdata, $vz->generate('2001:1620:2057:dead:beef::cafe:babe'));
$this->assertEquals($pngdata, $vz->generate('127.0.0.1'));
+
+ // generating new salt
+ $salt = serversalt::get();
+ require 'mcrypt_mock.php';
+ $this->assertNotEquals($salt, serversalt::generate());
}
}