Browse Source

Force JSON request for getting paste data

rugk 2 years ago
parent
commit
183ebe518b
8 changed files with 160 additions and 81 deletions
  1. 0 3
      .editorconfig
  2. 1 1
      INSTALL.md
  3. 144 66
      js/privatebin.js
  4. 1 1
      lib/Model/Paste.php
  5. 12 4
      lib/PrivateBin.php
  6. 0 2
      tpl/.editorconfig
  7. 1 2
      tpl/bootstrap.php
  8. 1 2
      tpl/page.php

+ 0 - 3
.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

+ 1 - 1
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

+ 144 - 66
js/privatebin.js

@@ -43,10 +43,21 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         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:
          * <pre>
          *     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=
          * </pre>
          *
@@ -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();
-
-                // if password is there, re-try
-                if (password.length === 0) {
-                    password = Prompt.requestPassword();
+                // show prompt
+                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);
 
@@ -3845,6 +3910,18 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
         }
 
         /**
+         * 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
          *
          * @name   Controller.showPaste
@@ -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();
+            }
+
+            // 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();
             }
 
-            // otherwise create a new paste
-            me.newPaste();
+            //display an existing paste
+            return me.showPaste();
         }
 
         return me;

+ 1 - 1
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=<pasteid>&deletetoken=<deletetoken>
+     * https://example.com/privatebin/?pasteid=<pasteid>&deletetoken=<deletetoken>
      *
      * @access public
      * @return string

+ 12 - 4
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;
             }

+ 0 - 2
tpl/.editorconfig

@@ -5,5 +5,3 @@ root = false
 # special format for PHP templates
 [*.php]
 indent_style = tab
-indent_size = 4
-

+ 1 - 2
tpl/bootstrap.php

@@ -69,7 +69,7 @@ if ($MARKDOWN):
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-7WGautcQxef6PeNh1sNcdCFCNRNo2uULN7QCgjqd+fWalRubtu1mtMEz8BLQ8sKgzPRF8E6dqgBQJ5ycwt03gA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-TCwmpH2nrRe3mLYIsGG90FGOFAqZal8SUQVNp16iA7K1B80I//RXp8mYxXdyyk/eluKilwCNY5wo9MYC4vdEoQ==" crossorigin="anonymous"></script>
 		<!--[if lt IE 10]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
 		<![endif]-->
@@ -478,7 +478,6 @@ endif;
 			</footer>
 		</main>
 		<div id="serverdata" class="hidden" aria-hidden="true">
-			<div id="cipherdata"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
 <?php
 if ($DISCUSSION):
 ?>

+ 1 - 2
tpl/page.php

@@ -47,7 +47,7 @@ if ($MARKDOWN):
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-7WGautcQxef6PeNh1sNcdCFCNRNo2uULN7QCgjqd+fWalRubtu1mtMEz8BLQ8sKgzPRF8E6dqgBQJ5ycwt03gA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-TCwmpH2nrRe3mLYIsGG90FGOFAqZal8SUQVNp16iA7K1B80I//RXp8mYxXdyyk/eluKilwCNY5wo9MYC4vdEoQ==" crossorigin="anonymous"></script>
 		<!--[if lt IE 10]>
 		<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
 		<![endif]-->
@@ -227,7 +227,6 @@ endif;
 			</div>
 		</section>
 		<div id="serverdata" class="hidden" aria-hidden="true">
-			<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
 <?php
 if ($DISCUSSION):
 ?>