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