From ce5207c8a00ebeda0ba29ccab23be0591cc2b2aa Mon Sep 17 00:00:00 2001 From: MildlyMeticulous <302576729+MildlyMeticulous@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:50:14 +0100 Subject: [PATCH] Only allow http(s) URLs in exported HTML links (fixes #1575) Embed URLs never pass through the markdown parser, so the regex change in #1568 did not cover them and a javascript: URL in embed.url or embed.author.url still reached a live href. Adds ExportContext.EnsureSafeUrl, which returns the URL only when it parses as an absolute http or https URI, and applies it at the four embed anchors. An unsafe URL now takes the existing else branch, so the title and author text still render, just without a link. HtmlMarkdownVisitor.VisitLinkAsync uses the same helper and renders the link children without an anchor when the scheme is not allowed, so the three link regexes in MarkdownParser are no longer the only thing keeping unsafe schemes out of the export. --- .../Exporting/ExportContext.cs | 6 +++++ .../Exporting/HtmlMarkdownVisitor.cs | 11 ++++++-- .../Exporting/MessageGroupTemplate.cshtml | 27 +++++++++++++------ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/DiscordChatExporter.Core/Exporting/ExportContext.cs b/DiscordChatExporter.Core/Exporting/ExportContext.cs index 3c7f5785..ae640a8a 100644 --- a/DiscordChatExporter.Core/Exporting/ExportContext.cs +++ b/DiscordChatExporter.Core/Exporting/ExportContext.cs @@ -120,6 +120,12 @@ internal class ExportContext(DiscordClient discord, ExportRequest request) public Color? TryGetUserColor(Snowflake id) => GetUserRoles(id).Where(r => r.Color is not null).Select(r => r.Color).FirstOrDefault(); + public static string? EnsureSafeUrl(string? url) => + Uri.TryCreate(url, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) + ? url + : null; + public async ValueTask ResolveAssetUrlAsync( string url, CancellationToken cancellationToken = default diff --git a/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs b/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs index 0104a6d8..e40dae7d 100644 --- a/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs +++ b/DiscordChatExporter.Core/Exporting/HtmlMarkdownVisitor.cs @@ -188,12 +188,19 @@ internal partial class HtmlMarkdownVisitor( .Groups[1] .Value; + var safeUrl = ExportContext.EnsureSafeUrl(link.Url); + if (string.IsNullOrWhiteSpace(safeUrl)) + { + await VisitAsync(link.Children, cancellationToken); + return; + } + buffer.Append( !string.IsNullOrWhiteSpace(linkedMessageId) // lang=html - ? $"""""" + ? $"""""" // lang=html - : $"""""" + : $"""""" ); await VisitAsync(link.Children, cancellationToken); diff --git a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml index b3c06837..9f3f3ea0 100644 --- a/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml +++ b/DiscordChatExporter.Core/Exporting/MessageGroupTemplate.cshtml @@ -24,6 +24,9 @@ string FormatDate(DateTimeOffset instant, string format = "g") => Context.FormatDate(instant, format); + string? EnsureSafeUrl(string? url) => + ExportContext.EnsureSafeUrl(url); + async ValueTask FormatMarkdownAsync(string markdown) => Context.Request.ShouldFormatMarkdown ? Html.Raw(await HtmlMarkdownVisitor.FormatAsync(Context, markdown, true, CancellationToken)) @@ -484,9 +487,10 @@ @if (!string.IsNullOrWhiteSpace(embed.Author.Name)) { - if (!string.IsNullOrWhiteSpace(embed.Author.Url)) + var authorUrl = EnsureSafeUrl(embed.Author.Url); + if (!string.IsNullOrWhiteSpace(authorUrl)) { - +
@embed.Author.Name
} @@ -502,9 +506,12 @@ @if (!string.IsNullOrWhiteSpace(embed.Title)) {
- @if (!string.IsNullOrWhiteSpace(embed.Url)) + @{ + var titleUrl = EnsureSafeUrl(embed.Url); + } + @if (!string.IsNullOrWhiteSpace(titleUrl)) { - +
@(await FormatEmbedMarkdownAsync(embed.Title))
} @@ -604,9 +611,10 @@ @if (!string.IsNullOrWhiteSpace(embed.Author.Name)) { - if (!string.IsNullOrWhiteSpace(embed.Author.Url)) + var authorUrl = EnsureSafeUrl(embed.Author.Url); + if (!string.IsNullOrWhiteSpace(authorUrl)) { - +
@embed.Author.Name
} @@ -622,9 +630,12 @@ @if (!string.IsNullOrWhiteSpace(embed.Title)) {
- @if (!string.IsNullOrWhiteSpace(embed.Url)) + @{ + var titleUrl = EnsureSafeUrl(embed.Url); + } + @if (!string.IsNullOrWhiteSpace(titleUrl)) { - +
@(await FormatEmbedMarkdownAsync(embed.Title))
}