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/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..599facc170
--- /dev/null
+++ b/ILSpy.Tests/UpdateServiceTests.cs
@@ -0,0 +1,109 @@
+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://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 = """
+
+
+ 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().BeNull();
+ }
+
+ 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 0859c65fa2..7fd6084514 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";
@@ -35,19 +36,44 @@ 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
});
- string data = await GetWithRedirectsAsync(client, UpdateUrl).ConfigureAwait(false);
+
+ 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);
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");
+
+ // 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 };
return LatestAvailableVersion;
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