diff --git a/lib/links.js b/lib/links.js new file mode 100644 index 00000000..0f6dffd2 --- /dev/null +++ b/lib/links.js @@ -0,0 +1,28 @@ +module.exports = { + serveLinkWarningPage: function (req, res) { + let targetURL = typeof req.query.url === 'string' ? req.query.url : '' + let noteURL = typeof req.query.note === 'string' ? req.query.note : '' + if (noteURL !== '' && !/^[\w-]+$/.test(noteURL)) { + noteURL = '' + } + let valid = false + try { + targetURL = decodeURIComponent(targetURL) + const parsed = new URL(targetURL) + valid = ['http:', 'https:'].includes(parsed.protocol) + targetURL = parsed.href + } catch (err) { + valid = false + } + res.set({ + 'Cache-Control': 'no-store' + }) + res.render('link.ejs', { + title: 'External link', + valid, + noteURL, + targetURL, + opengraph: [] + }) + } +} diff --git a/lib/web/baseRouter.js b/lib/web/baseRouter.js index df5e2777..9dfeffc7 100644 --- a/lib/web/baseRouter.js +++ b/lib/web/baseRouter.js @@ -8,6 +8,8 @@ const baseRouter = module.exports = Router() const errors = require('../errors') +const links = require('../links') + // get index baseRouter.get('/', response.showIndex) // get 403 forbidden @@ -22,3 +24,5 @@ baseRouter.get('/404', function (req, res) { baseRouter.get('/500', function (req, res) { errors.errorInternalError(res) }) +// get external link warning page +baseRouter.get('/_link', links.serveLinkWarningPage) diff --git a/locales/de.json b/locales/de.json index 6678d1ca..b587432f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -126,5 +126,13 @@ "Protected - Only owner can edit (forbid guests)": "Geschützt - Nur Besitzer kann bearbeiten (Gäste verboten)", "Private - Only owner can view & edit": "Privat - Nur Besitzer kann anschauen & bearbeiten", "changed": "verändert", - "created": "erstellt" + "created": "erstellt", + "External link": "Externer Link", + "You've clicked a link that points to an external page.": "Es wurde ein Link zu einer externen Seite angeklickt.", + "The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.": "Der Inhalt dieser Seite ist unabhängig von HedgeDoc. Dort keine HedgeDoc-Zugangsdaten eingeben.", + "Do you want to continue?": "Fortfahren?", + "Continue to external page": "Weiter zur externen Seite", + "Back to your note": "Zurück zur Notiz", + "Invalid link": "Ungültiger Link", + "The provided link is not a valid URL.": "Die angegebene URL ist nicht gültig." } \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 3d30b66e..faecada7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -126,5 +126,13 @@ "Protected - Only owner can edit (forbid guests)": "Protected - Only owner can edit (forbid guests)", "Private - Only owner can view & edit": "Private - Only owner can view & edit", "changed": "changed", - "created": "created" -} \ No newline at end of file + "created": "created", + "You've clicked a link that points to an external page.": "You've clicked a link that points to an external page.", + "External link": "External link", + "Invalid link": "Invalid link", + "Do you want to continue?": "Do you want to continue?", + "Continue to external page": "Continue to external page", + "Back to your note": "Back to your note", + "The provided link is not a valid URL.": "The provided link is not a valid URL.", + "The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.": "The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there." +} diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md index 06f7e1d4..f1f241f6 100644 --- a/public/docs/release-notes.md +++ b/public/docs/release-notes.md @@ -1,5 +1,11 @@ # Release Notes +## 1.x UNRELEASED + +### Enhancements + +- Added a warning page when clicking external links + ## 1.10.8 2026-04-15 ### Bugfixes diff --git a/public/js/extra.js b/public/js/extra.js index fc2405cd..cba0bfeb 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -557,10 +557,6 @@ export function postProcess (code) { html = html.replace(/@import url\(([^)]*)\);?/gi, '') $(value).html(html) }) - // link should open in new window or tab - // also add noopener to prevent clickjacking - // See details: https://mathiasbynens.github.io/rel-noopener/ - result.find('a:not([href^="#"]):not([target])').attr('target', '_blank').attr('rel', 'noopener') // If it's hashtag link then make it base uri independent result.find('a[href^="#"]').each((index, linkTag) => { @@ -568,6 +564,7 @@ export function postProcess (code) { currentLocation.hash = linkTag.hash linkTag.href = currentLocation.toString() }) + rewriteExternalLinks(result) // update continue line numbers const linenumberdivs = result.find('.gutter.linenumber').toArray() @@ -593,6 +590,30 @@ export function postProcess (code) { } window.postProcess = postProcess +// rewrite external links to go through the /_link warning page +export function rewriteExternalLinks (view) { + view.find('a[href]').each((index, linkTag) => { + const anchor = $(linkTag) + const href = anchor.attr('href') + if (!href || href.startsWith('#')) { + return + } + let parsed + try { + parsed = new URL(href) + } catch (err) { + return + } + // only rewrite links that have an absolute http(s) URL on a different origin + if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin === window.location.origin) { + return + } + const noteURL = window.location.pathname.split('/').pop() + anchor.attr('href', `${window.location.origin}/_link?url=${encodeURIComponent(parsed.href)}¬e=${noteURL}`) + }) +} +window.rewriteExternalLinks = rewriteExternalLinks + const domevents = Object.getOwnPropertyNames(document).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(document)))).concat(Object.getOwnPropertyNames(Object.getPrototypeOf(window))).filter(function (i) { return !i.indexOf('on') && (document[i] === null || typeof document[i] === 'function') }).filter(function (elem, pos, self) { diff --git a/public/js/index.js b/public/js/index.js index 9e37e1fd..dd5d66ca 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -55,6 +55,7 @@ import { renderTOC, renderTags, renderTitle, + rewriteExternalLinks, scrollToHash, smoothHashScroll, updateLastChange, @@ -3498,6 +3499,7 @@ function updateViewInner () { } removeDOMEvents(ui.area.markdown) finishView(ui.area.markdown) + rewriteExternalLinks(ui.area.markdown) autoLinkify(ui.area.markdown) deduplicatedHeaderId(ui.area.markdown) renderTOC(ui.area.markdown) diff --git a/public/views/link.ejs b/public/views/link.ejs new file mode 100644 index 00000000..62b4e04a --- /dev/null +++ b/public/views/link.ejs @@ -0,0 +1,41 @@ + + + + + <%- include('hedgedoc/head') %> + + + + + <%- include('hedgedoc/header') %> +
+
+

+ <% if (valid) { %> + <%= __('External link') %> + <% } else { %> + <%= __('Invalid link') %> + <% } %> +

+

+ <%= __('You\'ve clicked a link that points to an external page.') %> +
+ <%= __('The content of that page is unrelated to HedgeDoc. Do not enter your HedgeDoc credentials there.') %> +

+ <% if (valid) { %> +

<%- targetURL %>

+

<%= __('Do you want to continue?') %>

+

+ <%= __('Continue to external page') %> + <%= __('Back to your note') %> +

+ <% } else { %> +

<%= __('The provided link is not a valid URL.') %>

+

+ <%= __('Back to your note') %> +

+ <% } %> +
+
+ +