From 183ebe518b02c52b22d2ad5f472dbfcb862c83f2 Mon Sep 17 00:00:00 2001 From: rugk Date: Tue, 11 Apr 2017 16:34:13 +0200 Subject: [PATCH 01/14] Force JSON request for getting paste data --- .editorconfig | 3 - INSTALL.md | 2 +- js/privatebin.js | 208 ++++++++++++++++++++++++++++++-------------- lib/Model/Paste.php | 2 +- lib/PrivateBin.php | 16 +++- tpl/.editorconfig | 2 - tpl/bootstrap.php | 3 +- tpl/page.php | 3 +- 8 files changed, 159 insertions(+), 80 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2c9ddc3..86252fa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,6 @@ insert_final_newline = true [*.css] indent_style = tab -indent_size = 4 [*.js] indent_style = space @@ -23,7 +22,6 @@ indent_size = 4 [*.jsonld] indent_style = tab -indent_size = 4 [*.php] indent_style = space @@ -31,7 +29,6 @@ indent_size = 4 [*.{htm,html}] indent_style = tab -indent_size = 4 [*.{md,markdown}] indent_style = space diff --git a/INSTALL.md b/INSTALL.md index b627bc9..a4bb08c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -64,7 +64,7 @@ process (see also > #### PATH Example > Your PrivateBin installation lives in a subfolder called "paste" inside of > your document root. The URL looks like this: -> http://example.com/paste/ +> https://example.com/paste/ > > The full path of PrivateBin on your webserver is: > /home/example.com/htdocs/paste diff --git a/js/privatebin.js b/js/privatebin.js index 8cf7683..2d7ead1 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -42,11 +42,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var Helper = (function () { var me = {}; + /** + * list of UserAgents (parts) known to belong to a bot + * + * @private + * @enum {Object} + * @readonly + */ + var BadBotUA = [ + 'Bot', + 'bot' + ]; + /** * character to HTML entity lookup table * * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} - * @name Helper.entityMap * @private * @enum {Object} * @readonly @@ -160,7 +171,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * URLs to handle: *
          *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
-         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         *     https://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
          *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
          * 
* @@ -251,7 +262,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * get the current location (without search or hash part of the URL), - * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/ + * eg. https://example.com/path/?aaaa#bbbb --> https://example.com/path/ * * @name Helper.baseUri * @function @@ -295,6 +306,32 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { baseUri = null; } + /** + * checks whether this is a bot we dislike + * + * @name Helper.isBadBot + * @function + * @return {bool} + */ + me.isBadBot = function() { + /* + if ($.inArray(navigator.userAgent, BadBotUA) >= 0) { + return true; + } + */ + + // check whether a bot user agent part can be found in the current + // user agent + var arrayLength = BadBotUA.length; + for (var i = 0; i < arrayLength; i++) { + if (navigator.userAgent.indexOf(BadBotUA) >= 0) { + return true; + } + } + + return false; + } + return me; })(); @@ -689,7 +726,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var Model = (function () { var me = {}; - var $cipherData, + var pasteData = null, $templates; var id = null, symmetricKey = null; @@ -721,32 +758,53 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } /** - * check if cipher data was supplied + * returns the paste data (inlduing the cipher data) * - * @name Model.getCipherData - * @function - * @return boolean - */ - me.hasCipherData = function() - { - return (me.getCipherData().length > 0); - } - - /** - * returns the cipher data - * - * @name Model.getCipherData + * @name Model.getPasteData * @function + * @param {function} callback (optional) Called when data is available + * @param {function} useCache (optional) Whether to use the cache or + * force a data reload. Default: true * @return string */ - me.getCipherData = function() + me.getPasteData = function(callback, useCache) { - return $cipherData.text(); + // use cache if possible/allowed + if (useCache !== false && pasteData !== null) { + //execute callback + if (typeof callback === 'function') { + return callback(pasteData); + } + + // alternatively just using inline + return pasteData; + } + + // reload data + Uploader.prepare(); + Uploader.setUrl(Helper.baseUri() + '?' + me.getPasteId()); + + Uploader.setFailure(function (status, data) { + // revert loading status… + Alert.hideLoading(); + TopNav.showViewButtons(); + + // show error message + Alert.showError(Uploader.parseUploadError(status, data, 'getting paste data')); + }) + Uploader.setSuccess(function (status, data) { + pasteData = data; + + if (typeof callback === 'function') { + return callback(data); + } + }) + Uploader.run(); } /** * get the pastes unique identifier from the URL, - * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487 + * eg. https://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487 * * @name Model.getPasteId * @function @@ -819,7 +877,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.reset = function() { - $cipherData = $templates = id = symmetricKey = null; + pasteData = $templates = id = symmetricKey = null; } /** @@ -832,7 +890,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $cipherData = $('#cipherdata'); $templates = $('#templates'); } @@ -1333,8 +1390,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { if (pasteMetaData.burnafterreading) { // display paste "for your eyes only" if it is deleted - // actually remove paste, before we claim it is deleted - Controller.removePaste(Model.getPasteId(), 'burnafterreading'); + // the paste has been deleted when the JSOn with the ciohertext + // has been downloaded Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again."); $remainingTime.addClass('foryoureyesonly'); @@ -1462,7 +1519,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } /** - * getthe cached password + * get the cached password * * If you do not get a password with this function * (returns an empty string), use requestPassword. @@ -3572,7 +3629,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var me = {}; /** - * decrypt data or prompts for password in cvase of failure + * decrypt data or prompts for password in case of failure * * @name PasteDecrypter.decryptOrPromptPassword * @private @@ -3590,18 +3647,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // if it fails, request password if (plaindata.length === 0 && password.length === 0) { - // try to get cached password first - password = Prompt.getPassword(); + // show prompt + Prompt.requestPassword(); - // if password is there, re-try - if (password.length === 0) { - password = Prompt.requestPassword(); + // if password is there instantly (legacy method), re-try encryption + if (Prompt.getPassword().length !== 0) { + // recursive + // note: an infinite loop is prevented as the previous if + // clause checks whether a password is already set and ignores + // errors when a password has been passed + return decryptOrPromptPassword(key, password, cipherdata); } - // recursive - // note: an infinite loop is prevented as the previous if - // clause checks whether a password is already set and ignores - // errors when a password has been passed - return decryptOrPromptPassword.apply(key, password, cipherdata); + + // if password could not be received yet, the new modal is used, + // which uses asyncronous event-driven methods to get the password. + // Thus, we cannot do anything yet, we need to wait for the user + // input. + return false; } // if all tries failed, we can only return an error @@ -3615,7 +3677,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * decrypt the actual paste text * - * @name PasteDecrypter.decryptOrPromptPassword + * @name PasteDecrypter.decryptPaste * @private * @function * @param {object} paste - paste data in object form @@ -3627,7 +3689,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ function decryptPaste(paste, key, password, ignoreError) { - var plaintext + var plaintext; if (ignoreError === true) { plaintext = CryptTool.decipher(key, password, paste.data); } else { @@ -3738,7 +3800,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Alert.showLoading('Decrypting paste…', 0, 'cloud-download'); // @TODO icon maybe rotation-lock, but needs full Glyphicons if (typeof paste === 'undefined') { - paste = $.parseJSON(Model.getCipherData()); + // get cipher data and wait until it is available + Model.getPasteData(me.run); + return; } var key = Model.getPasteKey(), @@ -3761,10 +3825,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // ignore empty paste, as this is allowed when pasting attachments decryptPaste(paste, key, password, true); } else { - decryptPaste(paste, key, password); + if (decryptPaste(paste, key, password) === false) { + return false; + } } - // shows the remaining time (until) deletion PasteStatus.showRemainingTime(paste.meta); @@ -3844,6 +3909,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Alert.hideLoading(); } + /** + * shows how we much we love bots that execute JS ;) + * + * @name Controller.showBadBotMessage + * @function + */ + me.showBadBotMessage = function() + { + TopNav.hideAllButtons(); + Alert.showError('I love you too, bot…'); + } + /** * shows the loaded paste * @@ -3853,7 +3930,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showPaste = function() { try { - Model.getPasteId(); Model.getPasteKey(); } catch (err) { console.error(err); @@ -3882,26 +3958,17 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // save window position to restore it later var orgPosition = $(window).scrollTop(); - Uploader.prepare(); - Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId()); - - Uploader.setFailure(function (status, data) { - // revert loading status… - Alert.hideLoading(); - TopNav.showViewButtons(); - - // show error message - Alert.showError(Uploader.parseUploadError(status, data, 'refresh display')); - }) - Uploader.setSuccess(function (status, data) { - PasteDecrypter.run(data); - + Model.getPasteData(function (data) { // restore position window.scrollTo(0, orgPosition); + PasteDecrypter.run(data); + + // NOTE: could create problems as callback may be called + // asyncronously if PasteDecrypter e.g. needs to wait for a + // password being entered callback(); - }) - Uploader.run(); + }, false); // this false is important as it circumvents the cache } /** @@ -3959,6 +4026,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * @function * @param {string} pasteId * @param {string} deleteToken + * @deprecated not used anymore, de we still need it? */ me.removePaste = function(pasteId, deleteToken) { // unfortunately many web servers don't support DELETE (and PUT) out of the box @@ -3968,7 +4036,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Uploader.setUnencryptedData('deletetoken', deleteToken); Uploader.setFailure(function () { - Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); + Alert.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); }) Uploader.run(); } @@ -4000,13 +4068,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { UiHelper.init(); Uploader.init(); - // display an existing paste - if (Model.hasCipherData()) { - return me.showPaste(); + // check whether existing paste needs to be shown + try { + Model.getPasteId(); + } catch (e) { + // otherwise create a new paste + return me.newPaste(); } - // otherwise create a new paste - me.newPaste(); + // prevent bots from viewing a paste and potentially deleting data + // when burn-after-reading is set + // see https://github.com/elrido/ZeroBin/issues/11 + if (Helper.isBadBot()) { + return me.showBadBotMessage(); + } + + //display an existing paste + return me.showPaste(); } return me; diff --git a/lib/Model/Paste.php b/lib/Model/Paste.php index 8f171fe..938d184 100644 --- a/lib/Model/Paste.php +++ b/lib/Model/Paste.php @@ -158,7 +158,7 @@ class Paste extends AbstractModel * * The token is the hmac of the pastes ID signed with the server salt. * The paste can be deleted by calling: - * http://example.com/privatebin/?pasteid=&deletetoken= + * https://example.com/privatebin/?pasteid=&deletetoken= * * @access public * @return string diff --git a/lib/PrivateBin.php b/lib/PrivateBin.php index c817445..30283cd 100644 --- a/lib/PrivateBin.php +++ b/lib/PrivateBin.php @@ -147,7 +147,10 @@ class PrivateBin ); break; case 'read': - $this->_read($this->_request->getParam('pasteid')); + // reading paste is disallowed in HTML display + if ($this->_request->isJsonApiCall()) { + $this->_read($this->_request->getParam('pasteid')); + } break; case 'jsonld': $this->_jsonld($this->_request->getParam('jsonld')); @@ -328,10 +331,10 @@ class PrivateBin // deleted if it has already expired $burnafterreading = $paste->isBurnafterreading(); if ( - ($burnafterreading && $deletetoken == 'burnafterreading') || - Filter::slowEquals($deletetoken, $paste->getDeleteToken()) + ($burnafterreading && $deletetoken == 'burnafterreading') || // either we burn-after it has been read //@TODO: not needed anymore now? + Filter::slowEquals($deletetoken, $paste->getDeleteToken()) // or we manually delete it with this secret token ) { - // Paste exists and deletion token is valid: Delete the paste. + // Paste exists and deletion token (if required) is valid: Delete the paste. $paste->delete(); $this->_status = 'Paste was properly deleted.'; } else { @@ -373,6 +376,11 @@ class PrivateBin unset($data->meta->salt); } $this->_data = json_encode($data); + + // If the paste was meant to be read only once, delete it. + if ($paste->isBurnafterreading()) { + $paste->delete(); + } } else { $this->_error = self::GENERIC_ERROR; } diff --git a/tpl/.editorconfig b/tpl/.editorconfig index 30c7ad2..9159bf4 100644 --- a/tpl/.editorconfig +++ b/tpl/.editorconfig @@ -5,5 +5,3 @@ root = false # special format for PHP templates [*.php] indent_style = tab -indent_size = 4 - diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 6fc01bd..6d1214e 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -69,7 +69,7 @@ if ($MARKDOWN): - + @@ -478,7 +478,6 @@ endif;