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],
i = 1;
return format.replace(/%(s|d)/g, function (m) {
// m is the matched format, e.g. %s, %d
let val = args[i];
if (m === '%d') {
val = parseFloat(val);
if (isNaN(val)) {
val = 0;
}
// A switch statement so that the formatter can be extended.
switch (m)
{
case '%d':
val = parseFloat(val);
if (isNaN(val)) {
val = 0;
}
break;
default:
// Default is %s
}
++i;
return val;
@ -540,23 +547,19 @@ jQuery.PrivateBin = (function($, RawDeflate) {
/**
* translate a string
*
* As the first parameter a jQuery element has to be provided, to let
* the text of this element be replaced. In case the (asynchronously
* Optionally pass a jQuery element as the first parameter, to automatically
* let the text of this element be replaced. In case the (asynchronously
* loaded) language is not downloadet yet, this will make sure the string
* is replaced when it is actually loaded. This also handles HTML in
* secure fashion, to avoid XSS.
* The second parameter is the message ID, matching the ones found in
* 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().
* is replaced when it is actually loaded.
* So for easy translations passing the jQuery object to apply it to is
* more save, especially when they are loaded in the beginning.
*
* @name I18n.translate
* @function
* @param {jQuery} $element
* @param {jQuery} $element - optional
* @param {string} messageId
* @param {...*} args - one or multiple parameters injected into placeholders
* @throws {string}
* @return {string}
*/
me.translate = function()
{
@ -570,8 +573,6 @@ jQuery.PrivateBin = (function($, RawDeflate) {
// optional jQuery element as first parameter
$element = args[0];
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
@ -632,10 +633,10 @@ jQuery.PrivateBin = (function($, RawDeflate) {
let containsLinks = args[0].indexOf('<a') !== -1;
// prevent double encoding, when we insert into a text node
if (!containsLinks) {
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) {
if (i > 0 || !containsLinks) {
args[i] = Helper.htmlEntities(args[i]);
}
}
@ -653,37 +654,18 @@ jQuery.PrivateBin = (function($, RawDeflate) {
);
}
if (containsLinks) {
$element.html(output);
} else {
// text node takes care of entity encoding
$element.text(output);
// if $element is given, insert translation
if ($element !== null) {
if (containsLinks) {
$element.html(output);
} else {
// text node takes care of entity encoding
$element.text(output);
}
return '';
}
};
/**
* 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();
return output;
};
/**

View File

@ -8,6 +8,78 @@ describe('I18n', function () {
$.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(
'replaces %s in strings with first given parameter into an element, encoding all, when no link is in the messageID',
'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 () {
before(function () {
$.PrivateBin.I18n.reset();
@ -178,9 +183,10 @@ describe('I18n', function () {
// mock
clean = jsdom('', {cookie: ['lang=' + language]});
$.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();
return language === result;
return language === result && language === alias;
}
);
@ -200,10 +206,11 @@ describe('I18n', function () {
$.PrivateBin.I18n.reset('en');
$.PrivateBin.I18n.loadTranslations();
var result = $.PrivateBin.I18n.translate2string('en');
var result = $.PrivateBin.I18n.translate('en'),
alias = $.PrivateBin.I18n._('en');
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/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="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" />

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/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="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" />