diff options
Diffstat (limited to 'debian/patches/CVE-2021-3427_2.patch')
-rw-r--r-- | debian/patches/CVE-2021-3427_2.patch | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/debian/patches/CVE-2021-3427_2.patch b/debian/patches/CVE-2021-3427_2.patch new file mode 100644 index 0000000..ec1edbe --- /dev/null +++ b/debian/patches/CVE-2021-3427_2.patch @@ -0,0 +1,475 @@ +commit 8ece036770484e170d5a728cdb25062088068f71 +Author: Calum Lind <calumlind+deluge@gmail.com> +Date: Mon Feb 14 10:23:19 2022 +0000 + + [WebUI] Move HTML entity encoding to client + + We should not be mangling the torrent data in the JSON API since this + can have unintended consquences with names and filepaths that can be + edited. If we escape those symbols in the JSON API then the data no + longer matches that stored by core. Therefore shift the encoding to the + client and consider dealing separetely with these entities when the user + first adds a torrent. + + * Created a modified htmlEncode in Deluge Formatter based on extjs + method that also encodes single quotes. + * Removed renderers in ListViews since only templates specified via tpl + are used and any render attribute specified was a no-op. + * Removed old buggy escapeHtml + + Resolves: https://dev.deluge-torrent.org/ticket/3459 + Ref: https://docs.sencha.com/extjs/6.5.3/modern/src/String.js.html#Ext.String-method-htmlEncode + Ref: https://docs.sencha.com/extjs/3.4.0/source/Format.html#Ext-util-Format-method-htmlEncode + +diff --git a/deluge/ui/web/js/deluge-all/Deluge.js b/deluge/ui/web/js/deluge-all/Deluge.js +index 86cae6d89..260ad978f 100644 +--- a/deluge/ui/web/js/deluge-all/Deluge.js ++++ b/deluge/ui/web/js/deluge-all/Deluge.js +@@ -25,11 +25,6 @@ Ext.state.Manager.setProvider( + // Add some additional functions to ext and setup some of the + // configurable parameters + Ext.apply(Ext, { +- escapeHTML: function (text) { +- text = String(text).replace('<', '<').replace('>', '>'); +- return text.replace('&', '&'); +- }, +- + isObjectEmpty: function (obj) { + for (var i in obj) { + return false; +diff --git a/deluge/ui/web/js/deluge-all/Formatters.js b/deluge/ui/web/js/deluge-all/Formatters.js +index 443bfdf56..6b09abee5 100644 +--- a/deluge/ui/web/js/deluge-all/Formatters.js ++++ b/deluge/ui/web/js/deluge-all/Formatters.js +@@ -15,7 +15,23 @@ + * @version 1.3 + * @singleton + */ +-Deluge.Formatters = { ++Deluge.Formatters = (function () { ++ var charToEntity = { ++ '&': '&', ++ '>': '>', ++ '<': '<', ++ '"': '"', ++ "'": ''', ++ }; ++ ++ var charToEntityRegex = new RegExp( ++ '(' + Object.keys(charToEntity).join('|') + ')', ++ 'g' ++ ); ++ var htmlEncodeReplaceFn = function (match, capture) { ++ return charToEntity[capture]; ++ }; ++ + /** + * Formats a date string in the date representation of the current locale, + * based on the systems timezone. +@@ -24,154 +40,162 @@ Deluge.Formatters = { + * @return {String} a string in the date representation of the current locale + * or "" if seconds < 0. + */ +- date: function (timestamp) { +- function zeroPad(num, count) { +- var numZeropad = num + ''; +- while (numZeropad.length < count) { +- numZeropad = '0' + numZeropad; ++ return (Formatters = { ++ date: function (timestamp) { ++ function zeroPad(num, count) { ++ var numZeropad = num + ''; ++ while (numZeropad.length < count) { ++ numZeropad = '0' + numZeropad; ++ } ++ return numZeropad; ++ } ++ timestamp = timestamp * 1000; ++ var date = new Date(timestamp); ++ return String.format( ++ '{0}/{1}/{2} {3}:{4}:{5}', ++ zeroPad(date.getDate(), 2), ++ zeroPad(date.getMonth() + 1, 2), ++ date.getFullYear(), ++ zeroPad(date.getHours(), 2), ++ zeroPad(date.getMinutes(), 2), ++ zeroPad(date.getSeconds(), 2) ++ ); ++ }, ++ ++ /** ++ * Formats the bytes value into a string with KiB, MiB or GiB units. ++ * ++ * @param {Number} bytes the filesize in bytes ++ * @param {Boolean} showZero pass in true to displays 0 values ++ * @return {String} formatted string with KiB, MiB or GiB units. ++ */ ++ size: function (bytes, showZero) { ++ if (!bytes && !showZero) return ''; ++ bytes = bytes / 1024.0; ++ ++ if (bytes < 1024) { ++ return bytes.toFixed(1) + ' KiB'; ++ } else { ++ bytes = bytes / 1024; + } +- return numZeropad; +- } +- timestamp = timestamp * 1000; +- var date = new Date(timestamp); +- return String.format( +- '{0}/{1}/{2} {3}:{4}:{5}', +- zeroPad(date.getDate(), 2), +- zeroPad(date.getMonth() + 1, 2), +- date.getFullYear(), +- zeroPad(date.getHours(), 2), +- zeroPad(date.getMinutes(), 2), +- zeroPad(date.getSeconds(), 2) +- ); +- }, +- +- /** +- * Formats the bytes value into a string with KiB, MiB or GiB units. +- * +- * @param {Number} bytes the filesize in bytes +- * @param {Boolean} showZero pass in true to displays 0 values +- * @return {String} formatted string with KiB, MiB or GiB units. +- */ +- size: function (bytes, showZero) { +- if (!bytes && !showZero) return ''; +- bytes = bytes / 1024.0; +- +- if (bytes < 1024) { +- return bytes.toFixed(1) + ' KiB'; +- } else { +- bytes = bytes / 1024; +- } +- +- if (bytes < 1024) { +- return bytes.toFixed(1) + ' MiB'; +- } else { +- bytes = bytes / 1024; +- } +- +- return bytes.toFixed(1) + ' GiB'; +- }, +- +- /** +- * Formats the bytes value into a string with K, M or G units. +- * +- * @param {Number} bytes the filesize in bytes +- * @param {Boolean} showZero pass in true to displays 0 values +- * @return {String} formatted string with K, M or G units. +- */ +- sizeShort: function (bytes, showZero) { +- if (!bytes && !showZero) return ''; +- bytes = bytes / 1024.0; + +- if (bytes < 1024) { +- return bytes.toFixed(1) + ' K'; +- } else { +- bytes = bytes / 1024; +- } ++ if (bytes < 1024) { ++ return bytes.toFixed(1) + ' MiB'; ++ } else { ++ bytes = bytes / 1024; ++ } + +- if (bytes < 1024) { +- return bytes.toFixed(1) + ' M'; +- } else { +- bytes = bytes / 1024; +- } ++ return bytes.toFixed(1) + ' GiB'; ++ }, ++ ++ /** ++ * Formats the bytes value into a string with K, M or G units. ++ * ++ * @param {Number} bytes the filesize in bytes ++ * @param {Boolean} showZero pass in true to displays 0 values ++ * @return {String} formatted string with K, M or G units. ++ */ ++ sizeShort: function (bytes, showZero) { ++ if (!bytes && !showZero) return ''; ++ bytes = bytes / 1024.0; ++ ++ if (bytes < 1024) { ++ return bytes.toFixed(1) + ' K'; ++ } else { ++ bytes = bytes / 1024; ++ } + +- return bytes.toFixed(1) + ' G'; +- }, ++ if (bytes < 1024) { ++ return bytes.toFixed(1) + ' M'; ++ } else { ++ bytes = bytes / 1024; ++ } + +- /** +- * Formats a string to display a transfer speed utilizing {@link #size} +- * +- * @param {Number} bytes the number of bytes per second +- * @param {Boolean} showZero pass in true to displays 0 values +- * @return {String} formatted string with KiB, MiB or GiB units. +- */ +- speed: function (bytes, showZero) { +- return !bytes && !showZero ? '' : fsize(bytes, showZero) + '/s'; +- }, ++ return bytes.toFixed(1) + ' G'; ++ }, ++ ++ /** ++ * Formats a string to display a transfer speed utilizing {@link #size} ++ * ++ * @param {Number} bytes the number of bytes per second ++ * @param {Boolean} showZero pass in true to displays 0 values ++ * @return {String} formatted string with KiB, MiB or GiB units. ++ */ ++ speed: function (bytes, showZero) { ++ return !bytes && !showZero ? '' : fsize(bytes, showZero) + '/s'; ++ }, ++ ++ /** ++ * Formats a string to show time in a human readable form. ++ * ++ * @param {Number} time the number of seconds ++ * @return {String} a formatted time string. will return '' if seconds == 0 ++ */ ++ timeRemaining: function (time) { ++ if (time <= 0) { ++ return '∞'; ++ } ++ time = time.toFixed(0); ++ if (time < 60) { ++ return time + 's'; ++ } else { ++ time = time / 60; ++ } + +- /** +- * Formats a string to show time in a human readable form. +- * +- * @param {Number} time the number of seconds +- * @return {String} a formatted time string. will return '' if seconds == 0 +- */ +- timeRemaining: function (time) { +- if (time <= 0) { +- return '∞'; +- } +- time = time.toFixed(0); +- if (time < 60) { +- return time + 's'; +- } else { +- time = time / 60; +- } +- +- if (time < 60) { +- var minutes = Math.floor(time); +- var seconds = Math.round(60 * (time - minutes)); +- if (seconds > 0) { +- return minutes + 'm ' + seconds + 's'; ++ if (time < 60) { ++ var minutes = Math.floor(time); ++ var seconds = Math.round(60 * (time - minutes)); ++ if (seconds > 0) { ++ return minutes + 'm ' + seconds + 's'; ++ } else { ++ return minutes + 'm'; ++ } + } else { +- return minutes + 'm'; ++ time = time / 60; + } +- } else { +- time = time / 60; +- } +- +- if (time < 24) { +- var hours = Math.floor(time); +- var minutes = Math.round(60 * (time - hours)); +- if (minutes > 0) { +- return hours + 'h ' + minutes + 'm'; ++ ++ if (time < 24) { ++ var hours = Math.floor(time); ++ var minutes = Math.round(60 * (time - hours)); ++ if (minutes > 0) { ++ return hours + 'h ' + minutes + 'm'; ++ } else { ++ return hours + 'h'; ++ } + } else { +- return hours + 'h'; ++ time = time / 24; + } +- } else { +- time = time / 24; +- } +- +- var days = Math.floor(time); +- var hours = Math.round(24 * (time - days)); +- if (hours > 0) { +- return days + 'd ' + hours + 'h'; +- } else { +- return days + 'd'; +- } +- }, + +- /** +- * Simply returns the value untouched, for when no formatting is required. +- * +- * @param {Mixed} value the value to be displayed +- * @return the untouched value. +- */ +- plain: function (value) { +- return value; +- }, +- +- cssClassEscape: function (value) { +- return value.toLowerCase().replace('.', '_'); +- }, +-}; ++ var days = Math.floor(time); ++ var hours = Math.round(24 * (time - days)); ++ if (hours > 0) { ++ return days + 'd ' + hours + 'h'; ++ } else { ++ return days + 'd'; ++ } ++ }, ++ ++ /** ++ * Simply returns the value untouched, for when no formatting is required. ++ * ++ * @param {Mixed} value the value to be displayed ++ * @return the untouched value. ++ */ ++ plain: function (value) { ++ return value; ++ }, ++ ++ cssClassEscape: function (value) { ++ return value.toLowerCase().replace('.', '_'); ++ }, ++ ++ htmlEncode: function (value) { ++ return !value ++ ? value ++ : String(value).replace(charToEntityRegex, htmlEncodeReplaceFn); ++ }, ++ }); ++})(); + var fsize = Deluge.Formatters.size; + var fsize_short = Deluge.Formatters.sizeShort; + var fspeed = Deluge.Formatters.speed; +@@ -179,3 +203,4 @@ var ftime = Deluge.Formatters.timeRemaining; + var fdate = Deluge.Formatters.date; + var fplain = Deluge.Formatters.plain; + Ext.util.Format.cssClassEscape = Deluge.Formatters.cssClassEscape; ++Ext.util.Format.htmlEncode = Deluge.Formatters.htmlEncode; +diff --git a/deluge/ui/web/js/deluge-all/add/AddWindow.js b/deluge/ui/web/js/deluge-all/add/AddWindow.js +index 771543de3..f5f2fdf07 100644 +--- a/deluge/ui/web/js/deluge-all/add/AddWindow.js ++++ b/deluge/ui/web/js/deluge-all/add/AddWindow.js +@@ -64,20 +64,6 @@ Deluge.add.AddWindow = Ext.extend(Deluge.add.Window, { + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Add'), this.onAddClick, this); + +- function torrentRenderer(value, p, r) { +- if (r.data['info_hash']) { +- return String.format( +- '<div class="x-deluge-add-torrent-name">{0}</div>', +- value +- ); +- } else { +- return String.format( +- '<div class="x-deluge-add-torrent-name-loading">{0}</div>', +- value +- ); +- } +- } +- + this.list = new Ext.list.ListView({ + store: new Ext.data.SimpleStore({ + fields: [ +@@ -91,7 +77,6 @@ Deluge.add.AddWindow = Ext.extend(Deluge.add.Window, { + id: 'torrent', + width: 150, + sortable: true, +- renderer: torrentRenderer, + dataIndex: 'text', + tpl: new Ext.XTemplate( + '<div class="x-deluge-add-torrent-name">{text:htmlEncode}</div>' +diff --git a/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js b/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js +index ed2abdcdc..4cfed016b 100644 +--- a/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js ++++ b/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js +@@ -46,7 +46,6 @@ Deluge.preferences.PreferencesWindow = Ext.extend(Ext.Window, { + columns: [ + { + id: 'name', +- renderer: fplain, + dataIndex: 'name', + }, + ], +diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py +index e16c440ed..c487ddf3c 100644 +--- a/deluge/ui/web/json_api.py ++++ b/deluge/ui/web/json_api.py +@@ -13,7 +13,6 @@ + import tempfile + from base64 import b64encode + from types import FunctionType +-from xml.sax.saxutils import escape as xml_escape + + from twisted.internet import defer, reactor + from twisted.internet.defer import Deferred, DeferredList +@@ -375,8 +374,6 @@ class WebApi(JSONComponent): + methods available from the core RPC. + """ + +- XSS_VULN_KEYS = ['name', 'message', 'comment', 'tracker_status', 'peers'] +- + def __init__(self): + super().__init__('Web', depend=['SessionProxy']) + self.hostlist = HostList() +@@ -581,7 +578,7 @@ def _on_got_files(self, torrent, d): + paths = [] + info = {} + for index, torrent_file in enumerate(files): +- path = xml_escape(torrent_file['path']) ++ path = torrent_file['path'] + paths.append(path) + torrent_file['progress'] = file_progress[index] + torrent_file['priority'] = file_priorities[index] +@@ -618,25 +615,10 @@ def walk(path, item): + file_tree.walk(walk) + d.callback(file_tree.get_tree()) + +- def _on_torrent_status(self, torrent, d): +- for key in self.XSS_VULN_KEYS: +- try: +- if key == 'peers': +- for peer in torrent[key]: +- peer['client'] = xml_escape(peer['client']) +- else: +- torrent[key] = xml_escape(torrent[key]) +- except KeyError: +- pass +- d.callback(torrent) +- + @export + def get_torrent_status(self, torrent_id, keys): + """Get the status for a torrent, filtered by status keys.""" +- main_deferred = Deferred() +- d = component.get('SessionProxy').get_torrent_status(torrent_id, keys) +- d.addCallback(self._on_torrent_status, main_deferred) +- return main_deferred ++ return component.get('SessionProxy').get_torrent_status(torrent_id, keys) + + @export + def get_torrent_files(self, torrent_id): |