Просмотр исходного кода

several changes:
- added tests for all 4 cases: output to string or into element vs first param contains link or not
- cleaned up logic - skip HTML entity encoding only if we can ensure insertion to text node / when output to string, we always encode
- DOMpurify sanitizes gopher, ws & wss links, which we previosly had tested for

El RIDO 6 лет назад
Родитель
Сommit
685c354d0e
6 измененных файлов с 105 добавлено и 31 удалено
  1. 1 1
      js/common.js
  2. 25 19
      js/privatebin.js
  3. 1 1
      js/test/AttachmentViewer.js
  4. 76 8
      js/test/I18n.js
  5. 1 1
      tpl/bootstrap.php
  6. 1 1
      tpl/page.php

+ 1 - 1
js/common.js

@@ -36,7 +36,7 @@ var a2zString    = ['a','b','c','d','e','f','g','h','i','j','k','l','m',
             return c.toUpperCase();
         })
     ),
-    schemas = ['ftp','gopher','http','https','ws','wss'],
+    schemas = ['ftp','http','https'],
     supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'],
     mimeTypes = ['image/png', 'application/octet-stream'],
     formats = ['plaintext', 'markdown', 'syntaxhighlighting'],

+ 25 - 19
js/privatebin.js

@@ -631,28 +631,35 @@ jQuery.PrivateBin = (function($, RawDeflate) {
 
             // messageID may contain links, but should be from a trusted source (code or translation JSON files)
             let containsLinks = args[0].indexOf('<a') !== -1;
-            for (let i = 0; i < args.length; ++i) {
-                // parameters (i > 0) may never contain HTML as they may come from untrusted parties
-                if (i > 0 || !containsLinks) {
-                    args[i] = Helper.htmlEntities(args[i]);
+
+            // prevent double encoding, when we insert into a text node
+            if (!containsLinks || $element === null) {
+                for (let i = 0; i < args.length; ++i) {
+                    // parameters (i > 0) may never contain HTML as they may come from untrusted parties
+                    if (i > 0 || !containsLinks) {
+                        args[i] = Helper.htmlEntities(args[i]);
+                    }
                 }
             }
-
             // format string
             let output = Helper.sprintf.apply(this, args);
 
-            // if $element is given, apply text to element
+            if (containsLinks) {
+                // only allow tags/attributes we actually use in translations
+                output = DOMPurify.sanitize(
+                    output, {
+                        ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
+                        ALLOWED_ATTR: ['href', 'id']
+                    }
+                );
+            }
+
+            // if $element is given, insert translation
             if ($element !== null) {
                 if (containsLinks) {
-                    // only allow tags/attributes we actually use in our translations
-                    $element.html(
-                        DOMPurify.sanitize(output, {
-                            ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
-                            ALLOWED_ATTR: ['href', 'id']
-                        })
-                    );
+                    $element.html(output);
                 } else {
-                    // avoid HTML entity encoding if translation contains no links
+                    // text node takes care of entity encoding
                     $element.text(output);                    
                 }
             }
@@ -1946,11 +1953,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
          */
         me.createPasteNotification = function(url, deleteUrl)
         {
-            $('#pastelink').html(
-                I18n._(
-                    'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
-                    url, url
-                )
+            I18n._(
+                $('#pastelink'),
+                'Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>',
+                url, url
             );
             // save newly created element
             $pasteUrl = $('#pasteurl');

+ 1 - 1
js/test/AttachmentViewer.js

@@ -88,7 +88,7 @@ describe('AttachmentViewer', function () {
                 if (prefix.indexOf('<a') === -1 && postfix.indexOf('<a') === -1) {
                     result = $.PrivateBin.Helper.htmlEntities(prefix + filename + postfix);
                 } else {
-                    result = $('<div>').html(prefix + $.PrivateBin.Helper.htmlEntities(filename) + postfix).html();
+                    result = prefix + $.PrivateBin.Helper.htmlEntities(filename) + postfix;
                 }
                 if (filename.length) {
                     results.push(

+ 76 - 8
js/test/I18n.js

@@ -3,6 +3,7 @@ var common = require('../common');
 
 describe('I18n', function () {
     describe('translate', function () {
+        this.timeout(30000);
         before(function () {
             $.PrivateBin.I18n.reset();
         });
@@ -45,13 +46,13 @@ describe('I18n', function () {
             'string',
             function (prefix, params, postfix) {
                 prefix    =    prefix.replace(/%(s|d)/g, '%%');
-                params[0] = params[0].replace(/%(s|d)/g, '%%').replace(/%<a/g, '');
+                params[0] = params[0].replace(/%(s|d)/g, '%%').replace(/<a/g, '');
                 postfix   =   postfix.replace(/%(s|d)/g, '%%');
-                var translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
+                const translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
                 params.unshift(prefix + '%s' + postfix);
-                var result = $.PrivateBin.I18n.translate.apply(this, params);
+                const result = $.PrivateBin.I18n.translate.apply(this, params);
                 $.PrivateBin.I18n.reset();
-                var alias = $.PrivateBin.I18n._.apply(this, params);
+                const alias = $.PrivateBin.I18n._.apply(this, params);
                 $.PrivateBin.I18n.reset();
                 return translation === result && translation === alias;
             }
@@ -63,14 +64,81 @@ describe('I18n', function () {
             'string',
             function (prefix, params, postfix) {
                 prefix    =    prefix.replace(/%(s|d)/g, '%%');
-                params[0] = params[0].replace(/%(s|d)/g, '%%') + '<a/>';
+                params[0] = params[0].replace(/%(s|d)/g, '%%');
                 postfix   =   postfix.replace(/%(s|d)/g, '%%');
-                var translation = $.PrivateBin.Helper.htmlEntities(prefix) + params[0] + $.PrivateBin.Helper.htmlEntities(postfix);
+                const translation = DOMPurify.sanitize(
+                    prefix + $.PrivateBin.Helper.htmlEntities(params[0]) + '<a></a>' + postfix, {
+                        ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
+                        ALLOWED_ATTR: ['href', 'id']
+                    }
+                );
+                params.unshift(prefix + '%s<a></a>' + postfix);
+                const result = $.PrivateBin.I18n.translate.apply(this, params);
+                $.PrivateBin.I18n.reset();
+                const alias = $.PrivateBin.I18n._.apply(this, params);
+                $.PrivateBin.I18n.reset();
+                return translation === result && translation === alias;
+            }
+        );
+        jsc.property(
+            'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
+            'string',
+            '(small nearray) string',
+            'string',
+            function (prefix, params, postfix) {
+                prefix    =    prefix.replace(/%(s|d)/g, '%%');
+                params[0] = params[0].replace(/%(s|d)/g, '%%').replace(/<a/g, '');
+                postfix   =   postfix.replace(/%(s|d)/g, '%%');
+                const translation = $.PrivateBin.Helper.htmlEntities(prefix + params[0] + postfix);
                 params.unshift(prefix + '%s' + postfix);
-                var result = $.PrivateBin.I18n.translate.apply(this, params);
+                let clean = jsdom();
+                $('body').html('<div id="i18n"></div>');
+                params.unshift($('#i18n'));
+                $.PrivateBin.I18n.translate.apply(this, params);
+                const result = $('#i18n').text();
                 $.PrivateBin.I18n.reset();
-                var alias = $.PrivateBin.I18n._.apply(this, params);
+                clean();
+                clean = jsdom();
+                $('body').html('<div id="i18n"></div>');
+                params[0] = $('#i18n');
+                $.PrivateBin.I18n._.apply(this, params);
+                const alias = $('#i18n').text();
                 $.PrivateBin.I18n.reset();
+                clean();
+                return translation === result && translation === alias;
+            }
+        );
+        jsc.property(
+            'replaces %s in strings with first given parameter into an element, encoding params only, when a link is part of the messageID inserted',
+            'string',
+            '(small nearray) string',
+            'string',
+            function (prefix, params, postfix) {
+                prefix    =    prefix.replace(/%(s|d)/g, '%%');
+                params[0] = params[0].replace(/%(s|d)/g, '%%');
+                postfix   =   postfix.replace(/%(s|d)/g, '%%');
+                const translation = $('<div>').html(DOMPurify.sanitize(
+                    prefix + $.PrivateBin.Helper.htmlEntities(params[0]) + '<a></a>' + postfix, {
+                        ALLOWED_TAGS: ['a', 'br', 'i', 'span'],
+                        ALLOWED_ATTR: ['href', 'id']
+                    }
+                )).html();
+                let args = Array.prototype.slice.call(params);
+                args.unshift(prefix + '%s<a></a>' + postfix);
+                let clean = jsdom();
+                $('body').html('<div id="i18n"></div>');
+                args.unshift($('#i18n'));
+                $.PrivateBin.I18n.translate.apply(this, args);
+                const result = $('#i18n').html();
+                $.PrivateBin.I18n.reset();
+                clean();
+                clean = jsdom();
+                $('body').html('<div id="i18n"></div>');
+                args[0] = $('#i18n');
+                $.PrivateBin.I18n._.apply(this, args);
+                const alias = $('#i18n').html();
+                $.PrivateBin.I18n.reset();
+                clean();
                 return translation === result && translation === alias;
             }
         );

+ 1 - 1
tpl/bootstrap.php

@@ -72,7 +72,7 @@ endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.7.js" integrity="sha512-XjNEK1xwh7SJ/7FouwV4VZcGW9cMySL3SwNpXgrURLBcXXQYtZdqhGoNdEwx9vwLvFjUGDQVNgpOrTsXlSTiQg==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-NI8nepXnEpe180qcTGA7ndw76ybURZvHkXTEdgj8+lAPpiVeW3K76DcK2J1YhQSsMBA2olquoJKf7/DEJQE3DA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-s5eJk4kWlOK8dmXtEGUcWDKj97JAzHtTwFBU8U++rIdLNFBe7D669cec8JzEXWPiQNsHe4PLwshwqTOy/7yfyA==" crossorigin="anonymous"></script>
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />

+ 1 - 1
tpl/page.php

@@ -50,7 +50,7 @@ endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-2.0.7.js" integrity="sha512-XjNEK1xwh7SJ/7FouwV4VZcGW9cMySL3SwNpXgrURLBcXXQYtZdqhGoNdEwx9vwLvFjUGDQVNgpOrTsXlSTiQg==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-NI8nepXnEpe180qcTGA7ndw76ybURZvHkXTEdgj8+lAPpiVeW3K76DcK2J1YhQSsMBA2olquoJKf7/DEJQE3DA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-s5eJk4kWlOK8dmXtEGUcWDKj97JAzHtTwFBU8U++rIdLNFBe7D669cec8JzEXWPiQNsHe4PLwshwqTOy/7yfyA==" crossorigin="anonymous"></script>
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />
 		<link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />