From ed15ee9a88d75842b0c3bb9099dfdacad27fb06d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 17 Mar 2023 15:54:57 +0100 Subject: Adding upstream patches to fix XSS vulnerability in deluge-web [CVE-2021-3427] (Closes: #1019594). Signed-off-by: Daniel Baumann --- debian/patches/CVE-2021-3427_1.patch | 125 +++++++++ debian/patches/CVE-2021-3427_2.patch | 475 +++++++++++++++++++++++++++++++++++ debian/patches/series | 2 + 3 files changed, 602 insertions(+) create mode 100644 debian/patches/CVE-2021-3427_1.patch create mode 100644 debian/patches/CVE-2021-3427_2.patch diff --git a/debian/patches/CVE-2021-3427_1.patch b/debian/patches/CVE-2021-3427_1.patch new file mode 100644 index 0000000..fca4f58 --- /dev/null +++ b/debian/patches/CVE-2021-3427_1.patch @@ -0,0 +1,125 @@ +commit a5503c0c606e196f368a58ea3d1b8457e76a3a31 +Author: Calum Lind +Date: Mon Feb 14 18:00:23 2022 +0000 + + [WebUI] Fix encoding HTML entities for torrent attributes + + Ensure all torrent attributes that might contain malicious HTML entities + are encoded. + + By allowing HTML entities to be rendered it enable malicious torrent + files to perform XSS attacks. + + Resolves: https://dev.deluge-torrent.org/ticket/3459 + +diff --git a/deluge/ui/web/js/deluge-all/EditTrackersWindow.js b/deluge/ui/web/js/deluge-all/EditTrackersWindow.js +index f6733aaa6..178fd583f 100644 +--- a/deluge/ui/web/js/deluge-all/EditTrackersWindow.js ++++ b/deluge/ui/web/js/deluge-all/EditTrackersWindow.js +@@ -57,6 +57,7 @@ Deluge.EditTrackersWindow = Ext.extend(Ext.Window, { + header: _('Tracker'), + width: 0.9, + dataIndex: 'url', ++ tpl: new Ext.XTemplate('{url:htmlEncode}'), + }, + ], + columnSort: { +diff --git a/deluge/ui/web/js/deluge-all/FilterPanel.js b/deluge/ui/web/js/deluge-all/FilterPanel.js +index b6e5ec5ca..f1fade120 100644 +--- a/deluge/ui/web/js/deluge-all/FilterPanel.js ++++ b/deluge/ui/web/js/deluge-all/FilterPanel.js +@@ -171,5 +171,5 @@ Deluge.FilterPanel.templates = { + tracker_host: + '
{filter} ({count})
', ++ 'tracker/{filter});">{filter:htmlEncode} ({count})', + }; +diff --git a/deluge/ui/web/js/deluge-all/TorrentGrid.js b/deluge/ui/web/js/deluge-all/TorrentGrid.js +index 198ec279f..ded3fb03b 100644 +--- a/deluge/ui/web/js/deluge-all/TorrentGrid.js ++++ b/deluge/ui/web/js/deluge-all/TorrentGrid.js +@@ -17,7 +17,7 @@ + return String.format( + '
{1}
', + r.data['state'].toLowerCase(), +- value ++ Ext.util.Format.htmlEncode(value) + ); + } + function torrentSpeedRenderer(value) { +@@ -62,7 +62,7 @@ + '
{0}
', +- value ++ Ext.util.Format.htmlEncode(value) + ); + } + +diff --git a/deluge/ui/web/js/deluge-all/add/AddWindow.js b/deluge/ui/web/js/deluge-all/add/AddWindow.js +index a4aff067b..771543de3 100644 +--- a/deluge/ui/web/js/deluge-all/add/AddWindow.js ++++ b/deluge/ui/web/js/deluge-all/add/AddWindow.js +@@ -93,6 +93,9 @@ Deluge.add.AddWindow = Ext.extend(Deluge.add.Window, { + sortable: true, + renderer: torrentRenderer, + dataIndex: 'text', ++ tpl: new Ext.XTemplate( ++ '
{text:htmlEncode}
' ++ ), + }, + ], + stripeRows: true, +diff --git a/deluge/ui/web/js/deluge-all/add/FilesTab.js b/deluge/ui/web/js/deluge-all/add/FilesTab.js +index fed52282d..d712c023d 100644 +--- a/deluge/ui/web/js/deluge-all/add/FilesTab.js ++++ b/deluge/ui/web/js/deluge-all/add/FilesTab.js +@@ -28,6 +28,7 @@ Deluge.add.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, { + header: _('Filename'), + width: 295, + dataIndex: 'filename', ++ tpl: new Ext.XTemplate('{filename:htmlEncode}'), + }, + { + header: _('Size'), +diff --git a/deluge/ui/web/js/deluge-all/details/DetailsTab.js b/deluge/ui/web/js/deluge-all/details/DetailsTab.js +index fdb4f7f0d..f1da178b1 100644 +--- a/deluge/ui/web/js/deluge-all/details/DetailsTab.js ++++ b/deluge/ui/web/js/deluge-all/details/DetailsTab.js +@@ -91,7 +91,9 @@ Deluge.details.DetailsTab = Ext.extend(Ext.Panel, { + for (var field in this.fields) { + if (!Ext.isDefined(data[field])) continue; // This is a field we are not responsible for. + if (data[field] == this.oldData[field]) continue; +- this.fields[field].dom.innerHTML = Ext.escapeHTML(data[field]); ++ this.fields[field].dom.innerHTML = Ext.util.Format.htmlEncode( ++ data[field] ++ ); + } + this.oldData = data; + }, +diff --git a/deluge/ui/web/js/deluge-all/details/FilesTab.js b/deluge/ui/web/js/deluge-all/details/FilesTab.js +index edc388d19..60de832a6 100644 +--- a/deluge/ui/web/js/deluge-all/details/FilesTab.js ++++ b/deluge/ui/web/js/deluge-all/details/FilesTab.js +@@ -18,6 +18,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, { + header: _('Filename'), + width: 330, + dataIndex: 'filename', ++ tpl: new Ext.XTemplate('{filename:htmlEncode}'), + }, + { + header: _('Size'), +diff --git a/deluge/ui/web/js/deluge-all/details/PeersTab.js b/deluge/ui/web/js/deluge-all/details/PeersTab.js +index 66d4a4b95..a1919630d 100644 +--- a/deluge/ui/web/js/deluge-all/details/PeersTab.js ++++ b/deluge/ui/web/js/deluge-all/details/PeersTab.js +@@ -73,7 +73,7 @@ + header: _('Client'), + width: 125, + sortable: true, +- renderer: fplain, ++ renderer: 'htmlEncode', + dataIndex: 'client', + }, + { 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 +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( +- '
{0}
', +- value +- ); +- } else { +- return String.format( +- '
{0}
', +- 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( + '
{text:htmlEncode}
' +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): diff --git a/debian/patches/series b/debian/patches/series index 7af5bdb..73f280f 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -2,3 +2,5 @@ new_release_check.patch 0001-Fix-warning-related-to-gettext.patch setuptools-60.patch systemd-debian.patch +CVE-2021-3427_1.patch +CVE-2021-3427_2.patch -- cgit v1.2.3