From a6ab47abb141e7d490a56b891ea0a2a026f40d54 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Wed, 8 Apr 2026 11:09:45 +0200 Subject: [PATCH 1/5] Use releaseTag with fallback to downloadUrl in updates.xml --- ILSpy/Updates/UpdateService.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ILSpy/Updates/UpdateService.cs b/ILSpy/Updates/UpdateService.cs index 0859c65fa2..9999783643 100644 --- a/ILSpy/Updates/UpdateService.cs +++ b/ILSpy/Updates/UpdateService.cs @@ -28,6 +28,7 @@ namespace ICSharpCode.ILSpy.Updates { internal static class UpdateService { + const string ReleaseTagBaseUrl = "https://github.com/icsharpcode/ILSpy/releases/tag/"; static readonly Uri UpdateUrl = new Uri("https://icsharpcode.github.io/ILSpy/updates.xml"); const string band = "stable"; @@ -39,15 +40,32 @@ public static async Task GetLatestVersionAsync() UseProxy = true, UseDefaultCredentials = true }); + + // Issue #3707: Remove 301 redirect logic once ilspy.net CNAME gone string data = await GetWithRedirectsAsync(client, UpdateUrl).ConfigureAwait(false); XDocument doc = XDocument.Load(new StringReader(data)); var bands = doc.Root.Elements("band").ToList(); var currentBand = bands.FirstOrDefault(b => (string)b.Attribute("id") == band) ?? bands.First(); Version version = new Version((string)currentBand.Element("latestVersion")); - string url = (string)currentBand.Element("downloadUrl"); - if (!(url.StartsWith("http://", StringComparison.Ordinal) || url.StartsWith("https://", StringComparison.Ordinal))) - url = null; // don't accept non-urls + + string url = null; + string releaseTag = (string)currentBand.Element("releaseTag"); + + if (releaseTag != null) + { + url = ReleaseTagBaseUrl + releaseTag; + // Prevent path traversal: normalize the URI and verify it still starts with the expected base + if (!new Uri(url).AbsoluteUri.StartsWith(ReleaseTagBaseUrl, StringComparison.Ordinal)) + url = null; + } + else + { + // Issue #3707: Remove else branch fallback logic once releaseTag version has shipped + 6 months + url = (string)currentBand.Element("downloadUrl"); + if (!(url.StartsWith("http://", StringComparison.Ordinal) || url.StartsWith("https://", StringComparison.Ordinal))) + url = null; // don't accept non-urls + } LatestAvailableVersion = new AvailableVersionInfo { Version = version, DownloadUrl = url }; return LatestAvailableVersion; From cb9f10eac49ad08151203002bd5be1267ebf5df1 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Wed, 8 Apr 2026 12:02:03 +0200 Subject: [PATCH 2/5] Add tests --- ILSpy.Tests/ILSpy.Tests.csproj | 1 + ILSpy.Tests/UpdateServiceTests.cs | 89 +++++++++++++++++++++++++++++++ ILSpy/Updates/UpdateService.cs | 9 +++- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 ILSpy.Tests/UpdateServiceTests.cs diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj index 05382fc290..0f0273f44d 100644 --- a/ILSpy.Tests/ILSpy.Tests.csproj +++ b/ILSpy.Tests/ILSpy.Tests.csproj @@ -49,6 +49,7 @@ + diff --git a/ILSpy.Tests/UpdateServiceTests.cs b/ILSpy.Tests/UpdateServiceTests.cs new file mode 100644 index 0000000000..ffff5c5725 --- /dev/null +++ b/ILSpy.Tests/UpdateServiceTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using AwesomeAssertions; + +using ICSharpCode.ILSpy.Updates; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +[TestFixture] +public class UpdateServiceTests +{ + [Test] + public async Task GetLatestVersionAsync_UsesReleaseTag_WhenReleaseTagIsPresent() + { + const string xml = """ + + + 10.0.0.0 + v10.0 + https://example.com/ignored.zip + + + """; + + using var client = new HttpClient(new StubHttpMessageHandler(xml)); + + var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml")); + + result.Version.Should().Be(new Version(10, 0, 0, 0)); + result.DownloadUrl.Should().Be("https://github.com/icsharpcode/ILSpy/releases/tag/v10.0"); + } + + [Test] + public async Task GetLatestVersionAsync_ReturnsNullDownloadUrl_WhenReleaseTagContainsPathTraversalAttempt() + { + const string xml = """ + + + 10.0.0.0 + ../malicious + https://example.com/ignored.zip + + + """; + + using var client = new HttpClient(new StubHttpMessageHandler(xml)); + + var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml")); + + result.Version.Should().Be(new Version(10, 0, 0, 0)); + result.DownloadUrl.Should().BeNull(); + } + + [Test] + public async Task GetLatestVersionAsync_UsesDownloadUrl_WhenReleaseTagIsMissing() + { + const string xml = """ + + + 10.0.0.0 + https://example.com/ilspy.zip + + + """; + + using var client = new HttpClient(new StubHttpMessageHandler(xml)); + + var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml")); + + result.Version.Should().Be(new Version(10, 0, 0, 0)); + result.DownloadUrl.Should().Be("https://example.com/ilspy.zip"); + } + + sealed class StubHttpMessageHandler(string responseContent) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { + Content = new StringContent(responseContent) + }); + } + } +} diff --git a/ILSpy/Updates/UpdateService.cs b/ILSpy/Updates/UpdateService.cs index 9999783643..399e6cf65e 100644 --- a/ILSpy/Updates/UpdateService.cs +++ b/ILSpy/Updates/UpdateService.cs @@ -36,13 +36,18 @@ internal static class UpdateService public static async Task GetLatestVersionAsync() { - var client = new HttpClient(new HttpClientHandler() { + using var client = new HttpClient(new HttpClientHandler() { UseProxy = true, UseDefaultCredentials = true }); + return await GetLatestVersionAsync(client, UpdateUrl).ConfigureAwait(false); + } + + internal static async Task GetLatestVersionAsync(HttpClient client, Uri updateUrl) + { // Issue #3707: Remove 301 redirect logic once ilspy.net CNAME gone - string data = await GetWithRedirectsAsync(client, UpdateUrl).ConfigureAwait(false); + string data = await GetWithRedirectsAsync(client, updateUrl).ConfigureAwait(false); XDocument doc = XDocument.Load(new StringReader(data)); var bands = doc.Root.Elements("band").ToList(); From bc674b26612dc9b7acaceb327662e51694700ac3 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Wed, 8 Apr 2026 12:11:45 +0200 Subject: [PATCH 3/5] Prevent arbitrary downloadUrl - must start with BaseUrl as well --- ILSpy.Tests/UpdateServiceTests.cs | 22 +++++++++++++++++++++- ILSpy/Updates/UpdateService.cs | 7 +++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ILSpy.Tests/UpdateServiceTests.cs b/ILSpy.Tests/UpdateServiceTests.cs index ffff5c5725..b7bf78c6f9 100644 --- a/ILSpy.Tests/UpdateServiceTests.cs +++ b/ILSpy.Tests/UpdateServiceTests.cs @@ -59,6 +59,26 @@ public async Task GetLatestVersionAsync_ReturnsNullDownloadUrl_WhenReleaseTagCon [Test] public async Task GetLatestVersionAsync_UsesDownloadUrl_WhenReleaseTagIsMissing() + { + const string xml = """ + + + 10.0.0.0 + https://github.com/icsharpcode/ILSpy/releases/tag/v10.0 + + + """; + + using var client = new HttpClient(new StubHttpMessageHandler(xml)); + + var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml")); + + result.Version.Should().Be(new Version(10, 0, 0, 0)); + result.DownloadUrl.Should().Be("https://github.com/icsharpcode/ILSpy/releases/tag/v10.0"); + } + + [Test] + public async Task GetLatestVersionAsync_UsesDownloadUrl_ButFailsBecauseBaseUrlDoesntMatch() { const string xml = """ @@ -74,7 +94,7 @@ public async Task GetLatestVersionAsync_UsesDownloadUrl_WhenReleaseTagIsMissing( var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml")); result.Version.Should().Be(new Version(10, 0, 0, 0)); - result.DownloadUrl.Should().Be("https://example.com/ilspy.zip"); + result.DownloadUrl.Should().BeNull(); } sealed class StubHttpMessageHandler(string responseContent) : HttpMessageHandler diff --git a/ILSpy/Updates/UpdateService.cs b/ILSpy/Updates/UpdateService.cs index 399e6cf65e..7fd6084514 100644 --- a/ILSpy/Updates/UpdateService.cs +++ b/ILSpy/Updates/UpdateService.cs @@ -60,6 +60,7 @@ internal static async Task GetLatestVersionAsync(HttpClien if (releaseTag != null) { url = ReleaseTagBaseUrl + releaseTag; + // Prevent path traversal: normalize the URI and verify it still starts with the expected base if (!new Uri(url).AbsoluteUri.StartsWith(ReleaseTagBaseUrl, StringComparison.Ordinal)) url = null; @@ -68,8 +69,10 @@ internal static async Task GetLatestVersionAsync(HttpClien { // Issue #3707: Remove else branch fallback logic once releaseTag version has shipped + 6 months url = (string)currentBand.Element("downloadUrl"); - if (!(url.StartsWith("http://", StringComparison.Ordinal) || url.StartsWith("https://", StringComparison.Ordinal))) - url = null; // don't accept non-urls + + // Prevent arbitrary URLs: verify it starts with the expected base + if (!new Uri(url).AbsoluteUri.StartsWith(ReleaseTagBaseUrl, StringComparison.Ordinal)) + url = null; } LatestAvailableVersion = new AvailableVersionInfo { Version = version, DownloadUrl = url }; From 53a0549c6298239f3ab5a63801ff14631cfc5653 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Wed, 8 Apr 2026 13:07:33 +0200 Subject: [PATCH 4/5] Remove custom domain ilspy.net in end-user visible places --- ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template | 2 +- ILSpy.AddIn/source.extension.vsixmanifest.template | 2 +- doc/ILSpyAboutPage.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template b/ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template index 1290859b79..7ce9bfa5af 100644 --- a/ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template +++ b/ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template @@ -4,7 +4,7 @@ ILSpy 2022 Integrates the ILSpy decompiler into Visual Studio. - https://ilspy.net + https://github.com/icsharpcode/ILSpy/ LICENSE ILSpy-Large.ico ILSpy;IL;decompile;decompiler;decompilation;C#;CSharp;.NET;Productivity;Open Source;Free diff --git a/ILSpy.AddIn/source.extension.vsixmanifest.template b/ILSpy.AddIn/source.extension.vsixmanifest.template index d180057c86..d5d86893c7 100644 --- a/ILSpy.AddIn/source.extension.vsixmanifest.template +++ b/ILSpy.AddIn/source.extension.vsixmanifest.template @@ -4,7 +4,7 @@ ILSpy Integrates the ILSpy decompiler into Visual Studio. - https://ilspy.net + https://github.com/icsharpcode/ILSpy/ LICENSE ILSpy-Large.ico diff --git a/doc/ILSpyAboutPage.txt b/doc/ILSpyAboutPage.txt index 1f33ed918c..8c77968ef7 100644 --- a/doc/ILSpyAboutPage.txt +++ b/doc/ILSpyAboutPage.txt @@ -1,6 +1,6 @@ ILSpy is the open-source .NET assembly browser and decompiler. -Website: https://ilspy.net/ +Website: https://github.com/icsharpcode/ILSpy/ Found a bug? https://github.com/icsharpcode/ILSpy/issues/new/choose Copyright 2011-2026 AlphaSierraPapa for the ILSpy team From 1e5ddf3a3c178ace40ad6a15f0b9d683906a5727 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Wed, 8 Apr 2026 13:32:53 +0200 Subject: [PATCH 5/5] Fix xml root node --- ILSpy.Tests/UpdateServiceTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ILSpy.Tests/UpdateServiceTests.cs b/ILSpy.Tests/UpdateServiceTests.cs index b7bf78c6f9..599facc170 100644 --- a/ILSpy.Tests/UpdateServiceTests.cs +++ b/ILSpy.Tests/UpdateServiceTests.cs @@ -19,13 +19,13 @@ public class UpdateServiceTests public async Task GetLatestVersionAsync_UsesReleaseTag_WhenReleaseTagIsPresent() { const string xml = """ - + 10.0.0.0 v10.0 https://example.com/ignored.zip - + """; using var client = new HttpClient(new StubHttpMessageHandler(xml)); @@ -40,13 +40,13 @@ public async Task GetLatestVersionAsync_UsesReleaseTag_WhenReleaseTagIsPresent() public async Task GetLatestVersionAsync_ReturnsNullDownloadUrl_WhenReleaseTagContainsPathTraversalAttempt() { const string xml = """ - + 10.0.0.0 ../malicious https://example.com/ignored.zip - + """; using var client = new HttpClient(new StubHttpMessageHandler(xml)); @@ -61,12 +61,12 @@ public async Task GetLatestVersionAsync_ReturnsNullDownloadUrl_WhenReleaseTagCon public async Task GetLatestVersionAsync_UsesDownloadUrl_WhenReleaseTagIsMissing() { const string xml = """ - + 10.0.0.0 https://github.com/icsharpcode/ILSpy/releases/tag/v10.0 - + """; using var client = new HttpClient(new StubHttpMessageHandler(xml)); @@ -81,12 +81,12 @@ public async Task GetLatestVersionAsync_UsesDownloadUrl_WhenReleaseTagIsMissing( public async Task GetLatestVersionAsync_UsesDownloadUrl_ButFailsBecauseBaseUrlDoesntMatch() { const string xml = """ - + 10.0.0.0 https://example.com/ilspy.zip - + """; using var client = new HttpClient(new StubHttpMessageHandler(xml));