From c8b80c40410d7c7dc7a7748c1fc9cd1da8e32bb1 Mon Sep 17 00:00:00 2001
From: supahgreg
Date: Tue, 21 Oct 2025 04:43:21 +0000
Subject: [PATCH] Improve handling of URLs on the frontend.
---
js/App.js | 25 +++++++++++++++++++++++++
js/Article.js | 20 +++++++-------------
js/CommonDialogs.js | 4 ++--
js/Feeds.js | 2 +-
js/Headlines.js | 12 ++++++------
js/PrefHelpers.js | 4 ++--
6 files changed, 43 insertions(+), 24 deletions(-)
diff --git a/js/App.js b/js/App.js
index 69d98eea4..0dce1a5b7 100644
--- a/js/App.js
+++ b/js/App.js
@@ -427,6 +427,31 @@ const App = {
return p.replace(/[&<>"'/]/g, m => map[m]);
},
+ /**
+ * Sanitize a URL for safe use in href attributes and window.open()
+ * @param {string} url - URL to sanitize
+ * @param {string} fallback - Optional fallback value if URL is invalid (default: empty string)
+ * @return {string} Safe URL or fallback
+ */
+ sanitizeUrl: function(url, fallback = '') {
+ if (!url || typeof url !== 'string') return fallback;
+
+ // Remove NULL bytes and other control characters
+ // eslint-disable-next-line no-control-regex
+ const cleaned = url.replace(/[\x00-\x1F\x7F]/g, '');
+
+ const trimmed = cleaned.trim();
+ if (!trimmed) return fallback;
+
+ return /^https?:\/\/.+/i.test(trimmed) ? trimmed : fallback;
+ },
+ openUrl: function(url) {
+ const sanitized = this.sanitizeUrl(url);
+ if (sanitized) {
+ const w = window.open(sanitized);
+ w.opener = null;
+ }
+ },
unescapeHtml: function(p) {
if (typeof p !== 'string' || p.indexOf('&') === -1)
return p;
diff --git a/js/Article.js b/js/Article.js
index b061b4fdf..6bfe87c8d 100644
--- a/js/Article.js
+++ b/js/Article.js
@@ -76,12 +76,6 @@ const Article = {
}
}
},
- popupOpenUrl: function(url) {
- const w = window.open("");
-
- w.opener = null;
- w.location = url;
- },
cdmToggleGridSpan: function(id) {
const row = document.getElementById(`RROW-${id}`);
@@ -160,26 +154,26 @@ const Article = {
`
} else if (enc.content_type && enc.content_type.indexOf("audio/") !== -1 && App.audioCanPlay(enc.content_type)) {
return `
`;
} else {
return `
- ${App.escapeHtml(enc.content_url)}
`
}
} else {
return `
- ${App.escapeHtml(enc.content_url)}
`
@@ -191,7 +185,7 @@ const Article = {
${__('Attachments')}
${enclosures.entries.map((enc) => `
-
${enc.title ? enc.title : enc.filename}
@@ -233,7 +227,7 @@ const Article = {
comments_msg = hl.num_comments + " " + ngettext("comment", "comments", hl.num_comments)
}
- comments = `
(${comments_msg})`;
+ comments = `
(${comments_msg})`;
}
return comments;
@@ -288,7 +282,7 @@ const Article = {
+ href="${App.escapeHtml(App.sanitizeUrl(hl.link))}">${hl.title}
${hl.updated_long}
diff --git a/js/CommonDialogs.js b/js/CommonDialogs.js
index c33763f88..5709a7047 100644
--- a/js/CommonDialogs.js
+++ b/js/CommonDialogs.js
@@ -609,7 +609,7 @@ const CommonDialogs = {
-
+
) : )