From f6893d338b4cff2cd17d6dbe439e13f14592a6e3 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 13:34:51 +0000 Subject: [PATCH 01/10] refactor: use DOMParser for checking if translation is HTML --- js/privatebin.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 29f2dd44..c02767e2 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -963,13 +963,17 @@ jQuery.PrivateBin = (function($) { * @returns {boolean} */ function isStringContainsHtml(messageId) { - // An integer which specifies the type of the node. An Element node like

or

. - const elementNodeType = 1; - - const div = document.createElement('div'); - div.innerHTML = messageId; - - return Array.from(div.childNodes).some(node => node.nodeType === elementNodeType); + // Use DOMParser to parse the string as HTML. DOMParser does not + // execute scripts nor load external resources when parsing, making + // it safer against XSS. + try { + const doc = new DOMParser().parseFromString(String(messageId), 'text/html'); + return Array.from(doc.body.childNodes).some(node => node.nodeType === Node.ELEMENT_NODE); + } catch (e) { + // If parsing fails for any reason, consider it not HTML to avoid + // treating arbitrary strings as markup. + return false; + } } return me; From ad55131831307920b18f600f5eb5c04d384817cf Mon Sep 17 00:00:00 2001 From: rugk Date: Sat, 15 Nov 2025 09:57:39 +0000 Subject: [PATCH 02/10] refactor: use given HTML config for DOMPurify --- js/privatebin.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index c02767e2..f8a55cbe 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -812,12 +812,11 @@ jQuery.PrivateBin = (function($) { if (containsHtml) { // only allow tags/attributes we actually use in translations - output = DOMPurify.sanitize( - output, { + const sanitizeConfig = Object.assign({}, purifyHtmlConfig, { ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], ALLOWED_ATTR: ['href', 'id'] - } - ); + }); + output = DOMPurify.sanitize(output, sanitizeConfig); } // if $element is given, insert translation From ce06857d2cdd380bfc3cd73410cb1f63d85aae2b Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 27 Nov 2025 21:05:59 +0000 Subject: [PATCH 03/10] chore update SRI hash of main JS file --- lib/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Configuration.php b/lib/Configuration.php index 2cccc342..5bee1cab 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -121,7 +121,7 @@ class Configuration 'js/kjua-0.10.0.js' => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==', 'js/legacy.js' => 'sha512-rGXYUpIqbFoHAgBXZ0UlJBdNAIMOC9EQ67MG0X46D5uRB8LvwzgKirbSQRGdYfk8I2jsUcm+tvHXYboUnC6DUg==', 'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==', - 'js/privatebin.js' => 'sha512-ZwoUDxBdEE+zNoGqr9o7X7CJYS4JStEeNvcOnhz69YVbXjiibNoYSY7i3vc6MLI3M/K1K6sIUmSFm8sjoUdF5Q==', + 'js/privatebin.js' => 'sha512-lR/UzD67Pbg9nHDxDHxXktRKbXTQ96bDucbzbc0ELOLGzJHBqv1Qih+Aw+blBlAxXAdjuJDf9ch+R4m+i5bsQg==', 'js/purify-3.3.0.js' => 'sha512-lsHD5zxs4lu/NDzaaibe27Vd2t7Cy9JQ3qDHUvDfb4oZvKoWDNEhwUY+4bT3R68cGgpgCYp8U1x2ifeVxqurdQ==', 'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==', 'js/zlib-1.3.1-1.js' => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==', From 7e506c7f83f2f9d02178217565c10b47a3fef327 Mon Sep 17 00:00:00 2001 From: rugk Date: Mon, 23 Feb 2026 16:31:13 +0100 Subject: [PATCH 04/10] refactor: drastically simplify JS "has HTML" extension again Co-authored-by: El RIDO --- js/privatebin.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index f8a55cbe..58482b40 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -962,17 +962,9 @@ jQuery.PrivateBin = (function($) { * @returns {boolean} */ function isStringContainsHtml(messageId) { - // Use DOMParser to parse the string as HTML. DOMParser does not - // execute scripts nor load external resources when parsing, making - // it safer against XSS. - try { - const doc = new DOMParser().parseFromString(String(messageId), 'text/html'); - return Array.from(doc.body.childNodes).some(node => node.nodeType === Node.ELEMENT_NODE); - } catch (e) { - // If parsing fails for any reason, consider it not HTML to avoid - // treating arbitrary strings as markup. - return false; - } + // message IDs are allowed to contain anchors, spans, keyboard and emphasis tags + // we can recognize all of them by only checking for anchors and keyboard tags + return args[0].indexOf(' Date: Mon, 23 Feb 2026 16:36:46 +0100 Subject: [PATCH 05/10] refactor: use modern spread syntax for combining object Given it's 2026 this really should be supported by all browsers now: https://caniuse.com/mdn-javascript_operators_spread,mdn-javascript_operators_spread_spread_in_arrays,mdn-javascript_operators_spread_spread_in_function_calls,mdn-javascript_operators_spread_spread_in_object_literals --- js/privatebin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/privatebin.js b/js/privatebin.js index 58482b40..7d8ce842 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -812,7 +812,8 @@ jQuery.PrivateBin = (function($) { if (containsHtml) { // only allow tags/attributes we actually use in translations - const sanitizeConfig = Object.assign({}, purifyHtmlConfig, { + const sanitizeConfig = { + ...purifyHtmlConfig, ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], ALLOWED_ATTR: ['href', 'id'] }); From 69e37c2c04d1c44ec0dc2f4d64842974be5197f1 Mon Sep 17 00:00:00 2001 From: rugk Date: Mon, 23 Feb 2026 16:45:31 +0100 Subject: [PATCH 06/10] refactor: introduce purifyHtmlConfigStrictSubset --- js/privatebin.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 7d8ce842..37223997 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -58,9 +58,23 @@ jQuery.PrivateBin = (function($) { ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i, USE_PROFILES: { html: true - } + }, }; + /** + * DOMpurify settings for HTML content, where only a strict subset is allowed. + * + * NOTE: The key `USE_PROFILES` from {@see purifyHtmlConfig} needs to be excluded, + * as otherwise `USE_PROFILES` takes precedence. + * + * @private + */ + const purifyHtmlConfigStrictSubset { + ALLOWED_URI_REGEXP: purifyHtmlConfig.ALLOWED_URI_REGEXP, + ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], + ALLOWED_ATTR: ['href', 'id'] + }) + /** * DOMpurify settings for SVG content * @@ -439,7 +453,7 @@ jQuery.PrivateBin = (function($) { /(((https?|ftp):\/\/[\w?!=&.\/-;#@~%+*-]+(?![\w\s?!&.\/;#~%"=-]>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig, '$1' ), - purifyHtmlConfig + purifyHtmlConfigStrictSubset ) ); }; @@ -812,12 +826,7 @@ jQuery.PrivateBin = (function($) { if (containsHtml) { // only allow tags/attributes we actually use in translations - const sanitizeConfig = { - ...purifyHtmlConfig, - ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], - ALLOWED_ATTR: ['href', 'id'] - }); - output = DOMPurify.sanitize(output, sanitizeConfig); + output = DOMPurify.sanitize(output, purifyHtmlConfigStrictSubset); } // if $element is given, insert translation From 30f80d055bc9777de500f9923c3211b369e26bfa Mon Sep 17 00:00:00 2001 From: rugk Date: Mon, 23 Feb 2026 15:54:56 +0000 Subject: [PATCH 07/10] wipfix: fix JS syntax errors --- js/privatebin.js | 8 ++++---- lib/Configuration.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 371f70a2..01deba91 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -63,17 +63,17 @@ jQuery.PrivateBin = (function($) { /** * DOMpurify settings for HTML content, where only a strict subset is allowed. - * + * * NOTE: The key `USE_PROFILES` from {@see purifyHtmlConfig} needs to be excluded, * as otherwise `USE_PROFILES` takes precedence. * * @private */ - const purifyHtmlConfigStrictSubset { + const purifyHtmlConfigStrictSubset = { ALLOWED_URI_REGEXP: purifyHtmlConfig.ALLOWED_URI_REGEXP, ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], ALLOWED_ATTR: ['href', 'id'] - }) + }; /** * DOMpurify settings for SVG content @@ -977,7 +977,7 @@ jQuery.PrivateBin = (function($) { function isStringContainsHtml(messageId) { // message IDs are allowed to contain anchors, spans, keyboard and emphasis tags // we can recognize all of them by only checking for anchors and keyboard tags - return args[0].indexOf(' 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==', 'js/legacy.js' => 'sha512-RQEo1hxpNc37i+jz/D9/JiAZhG8GFx3+SNxjYnI7jUgirDIqrCSj6QPAAZeaidditcWzsJ3jxfEj5lVm7ZwTRQ==', 'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==', - 'js/privatebin.js' => 'sha512-4kyDedBvdmfL+0OQcVMkHIAsf4TMW8/iuKyQfYJYfjxc6lPYwFiBQo7Qvy6sILRnKy6TJoK0KmTFDDi1p83vHA==', + 'js/privatebin.js' => 'sha512-yhkiWE2QOk8EfAbeu9U4DVuium56VOHrIrKetgSe0dGaw+Tqxar1m3eGSxW9JJ0pC2uvP2+m49VDZ8e7iaLGVQ==', 'js/purify-3.3.0.js' => 'sha512-lsHD5zxs4lu/NDzaaibe27Vd2t7Cy9JQ3qDHUvDfb4oZvKoWDNEhwUY+4bT3R68cGgpgCYp8U1x2ifeVxqurdQ==', 'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==', 'js/zlib-1.3.1-2.js' => 'sha512-4gT+v+BkBqdVBbKOO4qKGOAzuay+v1FmOLksS+bMgQ08Oo4xEb3X48Xq1Kv2b4HtiCQA7xq9dFRzxal7jmQI7w==', From 5fd974e24700333e57c88b9f04ffd8c18495b573 Mon Sep 17 00:00:00 2001 From: rugk Date: Mon, 23 Feb 2026 16:00:22 +0000 Subject: [PATCH 08/10] style: fix/remove trailing comma --- js/privatebin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/privatebin.js b/js/privatebin.js index 01deba91..e4a6fc64 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -58,7 +58,7 @@ jQuery.PrivateBin = (function($) { ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i, USE_PROFILES: { html: true - }, + } }; /** From 1cc811644fff59ee73c5b9bc56ea863cae659761 Mon Sep 17 00:00:00 2001 From: rugk Date: Mon, 23 Feb 2026 16:00:31 +0000 Subject: [PATCH 09/10] docs: improve JSDoc --- js/privatebin.js | 4 ++-- lib/Configuration.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index e4a6fc64..7cf6872b 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -64,8 +64,8 @@ jQuery.PrivateBin = (function($) { /** * DOMpurify settings for HTML content, where only a strict subset is allowed. * - * NOTE: The key `USE_PROFILES` from {@see purifyHtmlConfig} needs to be excluded, - * as otherwise `USE_PROFILES` takes precedence. + * NOTE: The key {@link purifyHtmlConfig.USE_PROFILES} **must not** be included, + * as otherwise `USE_PROFILES` takes precedence over {@link purifyHtmlConfigStrictSubset.ALLOWED_TAGS}. * * @private */ diff --git a/lib/Configuration.php b/lib/Configuration.php index 95bfb1cd..a25228cd 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -122,7 +122,7 @@ class Configuration 'js/kjua-0.10.0.js' => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==', 'js/legacy.js' => 'sha512-RQEo1hxpNc37i+jz/D9/JiAZhG8GFx3+SNxjYnI7jUgirDIqrCSj6QPAAZeaidditcWzsJ3jxfEj5lVm7ZwTRQ==', 'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==', - 'js/privatebin.js' => 'sha512-yhkiWE2QOk8EfAbeu9U4DVuium56VOHrIrKetgSe0dGaw+Tqxar1m3eGSxW9JJ0pC2uvP2+m49VDZ8e7iaLGVQ==', + 'js/privatebin.js' => 'sha512-6SwOJniNN8RBmAK7yCt4ly2qYyH8OALxB74/K1AJgw+YnZgRCfTDVq1qY1K5Y2QCxCODGGTpAjTqQRExzCqV7g==', 'js/purify-3.3.0.js' => 'sha512-lsHD5zxs4lu/NDzaaibe27Vd2t7Cy9JQ3qDHUvDfb4oZvKoWDNEhwUY+4bT3R68cGgpgCYp8U1x2ifeVxqurdQ==', 'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==', 'js/zlib-1.3.1-2.js' => 'sha512-4gT+v+BkBqdVBbKOO4qKGOAzuay+v1FmOLksS+bMgQ08Oo4xEb3X48Xq1Kv2b4HtiCQA7xq9dFRzxal7jmQI7w==', From d874ea71a2fe668b9ca98a5c7a890043dc351bda Mon Sep 17 00:00:00 2001 From: rugk Date: Tue, 24 Feb 2026 12:06:46 +0000 Subject: [PATCH 10/10] test: fix tests --- js/test/I18n.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/test/I18n.js b/js/test/I18n.js index 503422e9..0def37ae 100644 --- a/js/test/I18n.js +++ b/js/test/I18n.js @@ -77,7 +77,8 @@ describe('I18n', function () { postfix = postfix.replace(/%(s|d)/g, '%%'); const translation = DOMPurify.sanitize( prefix + '' + postfix, { - ALLOWED_TAGS: ['a', 'i', 'span'], + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i, + ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], ALLOWED_ATTR: ['href', 'id'] } ); @@ -129,7 +130,8 @@ describe('I18n', function () { postfix = postfix.replace(/%(s|d)/g, '%%').trim(); const translation = DOMPurify.sanitize( prefix + '' + postfix, { - ALLOWED_TAGS: ['a', 'i', 'span'], + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|magnet):)/i, + ALLOWED_TAGS: ['a', 'i', 'span', 'kbd'], ALLOWED_ATTR: ['href', 'id'] } );