Revert "implement simplified translation logic, forcing the use of safe application via jQuery element"

This reverts commit 62365880b4. The unit tests showed that the text2string function completely undid the XSS fix, so it was always unsafe to use it. Also the logic simplifications were smaller then expected.
This commit is contained in:
El RIDO 2020-01-25 09:07:29 +01:00
parent 62365880b4
commit 29efc14aa7
No known key found for this signature in database
GPG Key ID: 0F5C940A6BD81F92
4 changed files with 111 additions and 122 deletions

View File

@ -322,12 +322,19 @@ jQuery.PrivateBin = (function($, RawDeflate) {
let format = args[0], let format = args[0],
i = 1; i = 1;
return format.replace(/%(s|d)/g, function (m) { return format.replace(/%(s|d)/g, function (m) {
// m is the matched format, e.g. %s, %d
let val = args[i]; let val = args[i];
if (m === '%d') { // A switch statement so that the formatter can be extended.
switch (m)
{
case '%d':
val = parseFloat(val); val = parseFloat(val);
if (isNaN(val)) { if (isNaN(val)) {
val = 0; val = 0;
} }
break;
default:
// Default is %s
} }
++i; ++i;
return val; return val;
@ -540,23 +547,19 @@ jQuery.PrivateBin = (function($, RawDeflate) {
/** /**
* translate a string * translate a string
* *
* As the first parameter a jQuery element has to be provided, to let * Optionally pass a jQuery element as the first parameter, to automatically
* the text of this element be replaced. In case the (asynchronously * let the text of this element be replaced. In case the (asynchronously
* loaded) language is not downloadet yet, this will make sure the string * loaded) language is not downloadet yet, this will make sure the string
* is replaced when it is actually loaded. This also handles HTML in * is replaced when it is actually loaded.
* secure fashion, to avoid XSS. * So for easy translations passing the jQuery object to apply it to is
* The second parameter is the message ID, matching the ones found in * more save, especially when they are loaded in the beginning.
* the translation files under the i18n directory.
* Any additional parameters will get inserted into the message ID in
* place of %s (strings) or %d (digits), applying the appropriate plural
* in case of digits. See also Helper.sprintf().
* *
* @name I18n.translate * @name I18n.translate
* @function * @function
* @param {jQuery} $element * @param {jQuery} $element - optional
* @param {string} messageId * @param {string} messageId
* @param {...*} args - one or multiple parameters injected into placeholders * @param {...*} args - one or multiple parameters injected into placeholders
* @throws {string} * @return {string}
*/ */
me.translate = function() me.translate = function()
{ {
@ -570,8 +573,6 @@ jQuery.PrivateBin = (function($, RawDeflate) {
// optional jQuery element as first parameter // optional jQuery element as first parameter
$element = args[0]; $element = args[0];
args.shift(); args.shift();
} else {
throw 'translation requires a jQuery element to be passed, for secure insertion of messages and to avoid double encoding of HTML entities';
} }
// extract messageId from arguments // extract messageId from arguments
@ -632,10 +633,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
let containsLinks = args[0].indexOf('<a') !== -1; let containsLinks = args[0].indexOf('<a') !== -1;
// prevent double encoding, when we insert into a text node // prevent double encoding, when we insert into a text node
if (!containsLinks) { if (!containsLinks || $element === null) {
for (let i = 0; i < args.length; ++i) { for (let i = 0; i < args.length; ++i) {
// parameters (i > 0) may never contain HTML as they may come from untrusted parties // parameters (i > 0) may never contain HTML as they may come from untrusted parties
if (i > 0) { if (i > 0 || !containsLinks) {
args[i] = Helper.htmlEntities(args[i]); args[i] = Helper.htmlEntities(args[i]);
} }
} }
@ -653,37 +654,18 @@ jQuery.PrivateBin = (function($, RawDeflate) {
); );
} }
// if $element is given, insert translation
if ($element !== null) {
if (containsLinks) { if (containsLinks) {
$element.html(output); $element.html(output);
} else { } else {
// text node takes care of entity encoding // text node takes care of entity encoding
$element.text(output); $element.text(output);
} }
}; return '';
}
/** return output;
* translate a string, outputs the result
*
* This function is identical to I18n.translate, but doesn't require a
* jQuery element as the first parameter, instead it returns the
* translated message as string.
* Avoid using this function, if possible, as it may double encode your
* message's HTML entities. This is done to fail safe, preventing XSS.
*
* @name I18n.translate2string
* @function
* @param {string} messageId
* @param {...*} args - one or multiple parameters injected into placeholders
* @throws {string}
* @return {string}
*/
me.translate2string = function()
{
let args = Array.prototype.slice.call(arguments),
$element = $('<textarea>');
args.unshift($element);
me.translate.apply(this, args);
return $element.text();
}; };
/** /**

View File

@ -8,6 +8,78 @@ describe('I18n', function () {
$.PrivateBin.I18n.reset(); $.PrivateBin.I18n.reset();
}); });
jsc.property(
'returns message ID unchanged if no translation found',
'string',
function (messageId) {
messageId = messageId.replace(/%(s|d)/g, '%%');
var plurals = [messageId, messageId + 's'],
fake = [messageId],
result = $.PrivateBin.I18n.translate(messageId);
$.PrivateBin.I18n.reset();
var alias = $.PrivateBin.I18n._(messageId);
$.PrivateBin.I18n.reset();
var pluralResult = $.PrivateBin.I18n.translate(plurals);
$.PrivateBin.I18n.reset();
var pluralAlias = $.PrivateBin.I18n._(plurals);
$.PrivateBin.I18n.reset();
var fakeResult = $.PrivateBin.I18n.translate(fake);
$.PrivateBin.I18n.reset();
var fakeAlias = $.PrivateBin.I18n._(fake);
$.PrivateBin.I18n.reset();
messageId = $.PrivateBin.Helper.htmlEntities(messageId);
return messageId === result && messageId === alias &&
messageId === pluralResult && messageId === pluralAlias &&
messageId === fakeResult && messageId === fakeAlias;
}
);
jsc.property(
'replaces %s in strings with first given parameter, 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);
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, encoding params only, when a link is part of 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, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%');
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( jsc.property(
'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID', 'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
'string', 'string',
@ -72,73 +144,6 @@ describe('I18n', function () {
); );
}); });
describe('translate2string', function () {
this.timeout(30000);
before(function () {
$.PrivateBin.I18n.reset();
});
jsc.property(
'returns message ID unchanged if no translation found',
'string',
function (messageId) {
messageId = messageId.replace(/%(s|d)/g, '%%');
let plurals = [messageId, messageId + 's'],
fake = [messageId],
clean = jsdom(),
result = $.PrivateBin.I18n.translate2string(messageId);
$.PrivateBin.I18n.reset();
var pluralResult = $.PrivateBin.I18n.translate2string(plurals);
$.PrivateBin.I18n.reset();
var fakeResult = $.PrivateBin.I18n.translate2string(fake);
$.PrivateBin.I18n.reset();
clean();
messageId = $.PrivateBin.Helper.htmlEntities(messageId);
return messageId === result && messageId === pluralResult && messageId === fakeResult;
}
);
jsc.property(
'replaces %s in strings with first given parameter, 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);
const result = $.PrivateBin.I18n.translate2string.apply(this, params);
$.PrivateBin.I18n.reset();
return translation === result;
}
);
jsc.property(
'replaces %s in strings with first given parameter, encoding params only, when a link is part of 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, '%%');
postfix = postfix.replace(/%(s|d)/g, '%%');
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.translate2string.apply(this, params);
$.PrivateBin.I18n.reset();
return translation === result;
}
);
});
describe('getPluralForm', function () { describe('getPluralForm', function () {
before(function () { before(function () {
$.PrivateBin.I18n.reset(); $.PrivateBin.I18n.reset();
@ -178,9 +183,10 @@ describe('I18n', function () {
// mock // mock
clean = jsdom('', {cookie: ['lang=' + language]}); clean = jsdom('', {cookie: ['lang=' + language]});
$.PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json')); $.PrivateBin.I18n.reset(language, require('../../i18n/' + language + '.json'));
var result = $.PrivateBin.I18n.translate2string('en'); var result = $.PrivateBin.I18n.translate('en'),
alias = $.PrivateBin.I18n._('en');
clean(); clean();
return language === result; return language === result && language === alias;
} }
); );
@ -200,10 +206,11 @@ describe('I18n', function () {
$.PrivateBin.I18n.reset('en'); $.PrivateBin.I18n.reset('en');
$.PrivateBin.I18n.loadTranslations(); $.PrivateBin.I18n.loadTranslations();
var result = $.PrivateBin.I18n.translate2string('en'); var result = $.PrivateBin.I18n.translate('en'),
alias = $.PrivateBin.I18n._('en');
clean(); clean();
return 'en' === result; return 'en' === result && 'en' === alias;
} }
); );
}); });

View File

@ -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/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/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-Pss+4+Yrpp5ZROFKS9VWpX13RUpVp2QXDNZtFzNrr7YV65D+iKKUeE1z/Sff887+3fyOyIhJwKMsdfnmXnaNkQ==" crossorigin="anonymous"></script> <script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-U9Au7V0FSY8S1xI6MrhPawEOFAPFejMI8PYlQNhC++XIQCQgQhYEqTYkhczN6F2MFAq/P1Hwn9A3IWaq9hu95g==" crossorigin="anonymous"></script>
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" /> <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-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" /> <link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />

View File

@ -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/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/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-Pss+4+Yrpp5ZROFKS9VWpX13RUpVp2QXDNZtFzNrr7YV65D+iKKUeE1z/Sff887+3fyOyIhJwKMsdfnmXnaNkQ==" crossorigin="anonymous"></script> <script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-U9Au7V0FSY8S1xI6MrhPawEOFAPFejMI8PYlQNhC++XIQCQgQhYEqTYkhczN6F2MFAq/P1Hwn9A3IWaq9hu95g==" crossorigin="anonymous"></script>
<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" /> <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-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" /> <link rel="icon" type="image/png" href="img/favicon-16x16.png?<?php echo rawurlencode($VERSION); ?>" sizes="16x16" />