introduce new zerobincompatibility option, replacing the base64 one, if it is enabled, delete tokens use sha256; added per paste salt with server salt fallback; this resolves the points 2.2 & 2.9 in #103

This commit is contained in:
El RIDO 2016-07-06 11:37:13 +02:00
parent 6b0b814dc6
commit 0e217a42c5
8 changed files with 87 additions and 38 deletions

View file

@ -39,10 +39,6 @@ template = "bootstrap"
; (optional) notice to display ; (optional) notice to display
; notice = "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service." ; notice = "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service."
; base64.js library version, defaults to 2.1.9
; use "1.7" if you are upgrading from a ZeroBin Alpha 0.19 installation
base64version = "2.1.9"
; by default ZeroBin will guess the visitors language based on the browsers ; by default ZeroBin will guess the visitors language based on the browsers
; settings. Optionally you can enable the language selection menu, which uses ; settings. Optionally you can enable the language selection menu, which uses
; a session cookie to store the choice until the browser is closed. ; a session cookie to store the choice until the browser is closed.
@ -57,6 +53,11 @@ languageselection = false
; the pastes encryption key ; the pastes encryption key
; urlshortener = "https://shortener.example.com/api?link=" ; urlshortener = "https://shortener.example.com/api?link="
; stay compatible with ZeroBin Alpha 0.19, less secure
; if enabled will use base64.js version 1.7 instead of 2.1.9 and sha1 instead of
; sha256 in HMAC for the deletion token
zerobincompatibility = false
[expire] [expire]
; expire value that is selected per default ; expire value that is selected per default
; make sure the value exists in [expire_options] ; make sure the value exists in [expire_options]

View file

@ -41,10 +41,10 @@ class configuration
'sizelimit' => 2097152, 'sizelimit' => 2097152,
'template' => 'bootstrap', 'template' => 'bootstrap',
'notice' => '', 'notice' => '',
'base64version' => '2.1.9',
'languageselection' => false, 'languageselection' => false,
'languagedefault' => '', 'languagedefault' => '',
'urlshortener' => '', 'urlshortener' => '',
'zerobincompatibility' => false,
), ),
'expire' => array( 'expire' => array(
'default' => '1week', 'default' => '1week',

View file

@ -27,7 +27,7 @@ class model_paste extends model_abstract
public function get() public function get()
{ {
$this->_data = $this->_store->read($this->getId()); $this->_data = $this->_store->read($this->getId());
// See if paste has expired and delete it if neccessary. // check if paste has expired and delete it if neccessary.
if (property_exists($this->_data->meta, 'expire_date')) if (property_exists($this->_data->meta, 'expire_date'))
{ {
if ($this->_data->meta->expire_date < time()) if ($this->_data->meta->expire_date < time())
@ -52,6 +52,12 @@ class model_paste extends model_abstract
$this->_data->meta->formatter = $this->_conf->getKey('defaultformatter'); $this->_data->meta->formatter = $this->_conf->getKey('defaultformatter');
} }
} }
// support old paste format with server wide salt
if (!property_exists($this->_data->meta, 'salt'))
{
$this->_data->meta->salt = serversalt::get();
}
$this->_data->comments = array_values($this->getComments()); $this->_data->comments = array_values($this->getComments());
$this->_data->comment_count = count($this->_data->comments); $this->_data->comment_count = count($this->_data->comments);
$this->_data->comment_offset = 0; $this->_data->comment_offset = 0;
@ -73,6 +79,7 @@ class model_paste extends model_abstract
throw new Exception('You are unlucky. Try again.', 75); throw new Exception('You are unlucky. Try again.', 75);
$this->_data->meta->postdate = time(); $this->_data->meta->postdate = time();
$this->_data->meta->salt = serversalt::generate();
// store paste // store paste
if ( if (
@ -151,7 +158,12 @@ class model_paste extends model_abstract
*/ */
public function getDeleteToken() public function getDeleteToken()
{ {
return hash_hmac('sha1', $this->getId(), serversalt::get()); if (!property_exists($this->_data->meta, 'salt')) $this->get();
return hash_hmac(
$this->_conf->getKey('zerobincompatibility') ? 'sha1' : 'sha256',
$this->getId(),
$this->_data->meta->salt
);
} }
/** /**

View file

@ -327,7 +327,6 @@ class zerobin
else else
{ {
// Make sure the token is valid. // Make sure the token is valid.
serversalt::setPath($this->_conf->getKey('dir', 'traffic'));
if (filter::slow_equals($deletetoken, $paste->getDeleteToken())) if (filter::slow_equals($deletetoken, $paste->getDeleteToken()))
{ {
// Paste exists and deletion token is valid: Delete the paste. // Paste exists and deletion token is valid: Delete the paste.
@ -364,6 +363,7 @@ class zerobin
{ {
$data = $paste->get(); $data = $paste->get();
$this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date'); $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date');
if (property_exists($data->meta, 'salt')) unset($data->meta->salt);
$this->_data = json_encode($data); $this->_data = json_encode($data);
} }
else else
@ -439,7 +439,7 @@ class zerobin
$page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected')); $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
$page->assign('PASSWORD', $this->_conf->getKey('password')); $page->assign('PASSWORD', $this->_conf->getKey('password'));
$page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload')); $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
$page->assign('BASE64JSVERSION', $this->_conf->getKey('base64version')); $page->assign('BASE64JSVERSION', $this->_conf->getKey('zerobincompatibility') ? '1.7' : '2.1.9');
$page->assign('LANGUAGESELECTION', $languageselection); $page->assign('LANGUAGESELECTION', $languageselection);
$page->assign('LANGUAGES', i18n::getLanguageLabels(i18n::getAvailableLanguages())); $page->assign('LANGUAGES', i18n::getLanguageLabels(i18n::getAvailableLanguages()));
$page->assign('EXPIRE', $expire); $page->assign('EXPIRE', $expire);

View file

@ -85,6 +85,7 @@ class helper
public static function getPasteWithAttachment($meta = array()) public static function getPasteWithAttachment($meta = array())
{ {
$example = self::$paste; $example = self::$paste;
$example['meta']['salt'] = serversalt::generate();
$example['meta'] = array_merge($example['meta'], $meta); $example['meta'] = array_merge($example['meta'], $meta);
return $example; return $example;
} }
@ -97,6 +98,8 @@ class helper
public static function getPasteAsJson($meta = array()) public static function getPasteAsJson($meta = array())
{ {
$example = self::getPaste(); $example = self::getPaste();
// the JSON shouldn't contain the salt
unset($example['meta']['salt']);
if (count($meta)) if (count($meta))
$example['meta'] = $meta; $example['meta'] = $meta;
$example['comments'] = array(); $example['comments'] = array();

View file

@ -13,10 +13,10 @@ class configurationTest extends PHPUnit_Framework_TestCase
'sizelimit' => 2097152, 'sizelimit' => 2097152,
'template' => 'bootstrap', 'template' => 'bootstrap',
'notice' => '', 'notice' => '',
'base64version' => '2.1.9',
'languageselection' => false, 'languageselection' => false,
'languagedefault' => '', 'languagedefault' => '',
'urlshortener' => '', 'urlshortener' => '',
'zerobincompatibility' => false,
), ),
'expire' => array( 'expire' => array(
'default' => '1week', 'default' => '1week',

View file

@ -46,13 +46,14 @@ class jsonApiTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
} }
/** /**
@ -80,13 +81,14 @@ class jsonApiTest extends PHPUnit_Framework_TestCase
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertEquals(helper::getPasteId(), $response['id'], 'outputted paste ID matches input'); $this->assertEquals(helper::getPasteId(), $response['id'], 'outputted paste ID matches input');
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
} }
/** /**
@ -97,9 +99,10 @@ class jsonApiTest extends PHPUnit_Framework_TestCase
$this->reset(); $this->reset();
$this->_model->create(helper::getPasteId(), helper::getPaste()); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$paste = $this->_model->read(helper::getPasteId());
$file = tempnam(sys_get_temp_dir(), 'FOO'); $file = tempnam(sys_get_temp_dir(), 'FOO');
file_put_contents($file, http_build_query(array( file_put_contents($file, http_build_query(array(
'deletetoken' => hash_hmac('sha1', helper::getPasteId(), serversalt::get()), 'deletetoken' => hash_hmac('sha256', helper::getPasteId(), $paste->meta->salt),
))); )));
request::setInputStream($file); request::setInputStream($file);
$_SERVER['QUERY_STRING'] = helper::getPasteId(); $_SERVER['QUERY_STRING'] = helper::getPasteId();
@ -121,9 +124,10 @@ class jsonApiTest extends PHPUnit_Framework_TestCase
$this->reset(); $this->reset();
$this->_model->create(helper::getPasteId(), helper::getPaste()); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$paste = $this->_model->read(helper::getPasteId());
$_POST = array( $_POST = array(
'action' => 'delete', 'action' => 'delete',
'deletetoken' => hash_hmac('sha1', helper::getPasteId(), serversalt::get()), 'deletetoken' => hash_hmac('sha256', helper::getPasteId(), $paste->meta->salt),
); );
$_SERVER['QUERY_STRING'] = helper::getPasteId(); $_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest'; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';

View file

@ -169,12 +169,13 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
} }
/** /**
@ -288,13 +289,13 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertGreaterThanOrEqual($time + 300, $paste->meta->expire_date, 'time is set correctly'); $this->assertGreaterThanOrEqual($time + 300, $paste->meta->expire_date, 'time is set correctly');
} }
@ -320,13 +321,13 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertGreaterThanOrEqual($time + 300, $paste->meta->expire_date, 'time is set correctly'); $this->assertGreaterThanOrEqual($time + 300, $paste->meta->expire_date, 'time is set correctly');
$this->assertEquals(1, $paste->meta->opendiscussion, 'discussion is enabled'); $this->assertEquals(1, $paste->meta->opendiscussion, 'discussion is enabled');
} }
@ -351,12 +352,13 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
} }
/** /**
@ -426,17 +428,17 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()),
$response['deletetoken'],
'outputs valid delete token'
);
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data'); $this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$original = json_decode(json_encode($_POST)); $original = json_decode(json_encode($_POST));
$stored = $this->_model->read($response['id']); $stored = $this->_model->read($response['id']);
foreach (array('data', 'attachment', 'attachmentname') as $key) { foreach (array('data', 'attachment', 'attachmentname') as $key) {
$this->assertEquals($original->$key, $stored->$key); $this->assertEquals($original->$key, $stored->$key);
} }
$this->assertEquals(
hash_hmac('sha256', $response['id'], $stored->meta->salt),
$response['deletetoken'],
'outputs valid delete token'
);
} }
/** /**
@ -459,12 +461,13 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals( $this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()), hash_hmac('sha256', $response['id'], $paste->meta->salt),
$response['deletetoken'], $response['deletetoken'],
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
} }
/** /**
@ -705,6 +708,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
unset($burnPaste['meta']['salt']);
$this->assertContains( $this->assertContains(
'<div id="cipherdata" class="hidden">' . '<div id="cipherdata" class="hidden">' .
htmlspecialchars(helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES) . htmlspecialchars(helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES) .
@ -796,6 +800,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$oldPaste['meta']['formatter'] = 'plaintext'; $oldPaste['meta']['formatter'] = 'plaintext';
unset($oldPaste['meta']['salt']);
$this->assertContains( $this->assertContains(
'<div id="cipherdata" class="hidden">' . '<div id="cipherdata" class="hidden">' .
htmlspecialchars(helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES) . htmlspecialchars(helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES) .
@ -813,8 +818,9 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$this->reset(); $this->reset();
$this->_model->create(helper::getPasteId(), helper::getPaste()); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$paste = $this->_model->read(helper::getPasteId());
$_GET['pasteid'] = helper::getPasteId(); $_GET['pasteid'] = helper::getPasteId();
$_GET['deletetoken'] = hash_hmac('sha1', helper::getPasteId(), serversalt::get()); $_GET['deletetoken'] = hash_hmac('sha256', helper::getPasteId(), $paste->meta->salt);
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@ -947,4 +953,27 @@ class zerobinTest extends PHPUnit_Framework_TestCase
); );
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
} }
/**
* @runInSeparateProcess
*/
public function testDeleteMissingPerPasteSalt()
{
$this->reset();
$paste = helper::getPaste();
unset($paste['meta']['salt']);
$this->_model->create(helper::getPasteId(), $paste);
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$_GET['pasteid'] = helper::getPasteId();
$_GET['deletetoken'] = hash_hmac('sha256', helper::getPasteId(), serversalt::get());
ob_start();
new zerobin;
$content = ob_get_contents();
$this->assertRegExp(
'#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
$content,
'outputs deleted status correctly'
);
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
}
} }