Browse Source

Use releaseTag with fallback to downloadUrl in updates.xml (#3713)

* Use releaseTag with fallback to downloadUrl in updates.xml
* Add tests
* Prevent arbitrary downloadUrl - must start with BaseUrl as well
* Remove custom domain ilspy.net in end-user visible places
pull/3715/head
Christoph Wille 2 weeks ago committed by GitHub
parent
commit
b5fc0d5b06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template
  2. 2
      ILSpy.AddIn/source.extension.vsixmanifest.template
  3. 1
      ILSpy.Tests/ILSpy.Tests.csproj
  4. 109
      ILSpy.Tests/UpdateServiceTests.cs
  5. 36
      ILSpy/Updates/UpdateService.cs
  6. 2
      doc/ILSpyAboutPage.txt

2
ILSpy.AddIn.VS2022/source.extension.vsixmanifest.template

@ -4,7 +4,7 @@
<Identity Id="ebf12ca7-a1fd-4aee-a894-4a0c5682fc2f" Version="$INSERTVERSION$" Language="en-US" Publisher="SharpDevelop Team" /> <Identity Id="ebf12ca7-a1fd-4aee-a894-4a0c5682fc2f" Version="$INSERTVERSION$" Language="en-US" Publisher="SharpDevelop Team" />
<DisplayName>ILSpy 2022</DisplayName> <DisplayName>ILSpy 2022</DisplayName>
<Description xml:space="preserve">Integrates the ILSpy decompiler into Visual Studio.</Description> <Description xml:space="preserve">Integrates the ILSpy decompiler into Visual Studio.</Description>
<MoreInfo>https://ilspy.net</MoreInfo> <MoreInfo>https://github.com/icsharpcode/ILSpy/</MoreInfo>
<License>LICENSE</License> <License>LICENSE</License>
<Icon>ILSpy-Large.ico</Icon> <Icon>ILSpy-Large.ico</Icon>
<Tags>ILSpy;IL;decompile;decompiler;decompilation;C#;CSharp;.NET;Productivity;Open Source;Free</Tags> <Tags>ILSpy;IL;decompile;decompiler;decompilation;C#;CSharp;.NET;Productivity;Open Source;Free</Tags>

2
ILSpy.AddIn/source.extension.vsixmanifest.template

@ -4,7 +4,7 @@
<Identity Id="a9120dbe-164a-4891-842f-fb7829273838" Version="$INSERTVERSION$" Language="en-US" Publisher="ic#code" /> <Identity Id="a9120dbe-164a-4891-842f-fb7829273838" Version="$INSERTVERSION$" Language="en-US" Publisher="ic#code" />
<DisplayName>ILSpy</DisplayName> <DisplayName>ILSpy</DisplayName>
<Description xml:space="preserve">Integrates the ILSpy decompiler into Visual Studio.</Description> <Description xml:space="preserve">Integrates the ILSpy decompiler into Visual Studio.</Description>
<MoreInfo>https://ilspy.net</MoreInfo> <MoreInfo>https://github.com/icsharpcode/ILSpy/</MoreInfo>
<License>LICENSE</License> <License>LICENSE</License>
<Icon>ILSpy-Large.ico</Icon> <Icon>ILSpy-Large.ico</Icon>
</Metadata> </Metadata>

1
ILSpy.Tests/ILSpy.Tests.csproj

@ -49,6 +49,7 @@
<Compile Include="Analyzers\TypeUsedByAnalyzerTests.cs" /> <Compile Include="Analyzers\TypeUsedByAnalyzerTests.cs" />
<Compile Include="CommandLineArgumentsTests.cs" /> <Compile Include="CommandLineArgumentsTests.cs" />
<Compile Include="ResourceReaderWriterTests.cs" /> <Compile Include="ResourceReaderWriterTests.cs" />
<Compile Include="UpdateServiceTests.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

109
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 = """
<updateInfo>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<releaseTag>v10.0</releaseTag>
<downloadUrl>https://example.com/ignored.zip</downloadUrl>
</band>
</updateInfo>
""";
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 = """
<updateInfo>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<releaseTag>../malicious</releaseTag>
<downloadUrl>https://example.com/ignored.zip</downloadUrl>
</band>
</updateInfo>
""";
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 = """
<updateInfo>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<downloadUrl>https://github.com/icsharpcode/ILSpy/releases/tag/v10.0</downloadUrl>
</band>
</updateInfo>
""";
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 = """
<updateInfo>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<downloadUrl>https://example.com/ilspy.zip</downloadUrl>
</band>
</updateInfo>
""";
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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {
Content = new StringContent(responseContent)
});
}
}
}

36
ILSpy/Updates/UpdateService.cs

@ -28,6 +28,7 @@ namespace ICSharpCode.ILSpy.Updates
{ {
internal static class UpdateService 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"); static readonly Uri UpdateUrl = new Uri("https://icsharpcode.github.io/ILSpy/updates.xml");
const string band = "stable"; const string band = "stable";
@ -35,19 +36,44 @@ namespace ICSharpCode.ILSpy.Updates
public static async Task<AvailableVersionInfo> GetLatestVersionAsync() public static async Task<AvailableVersionInfo> GetLatestVersionAsync()
{ {
var client = new HttpClient(new HttpClientHandler() { using var client = new HttpClient(new HttpClientHandler() {
UseProxy = true, UseProxy = true,
UseDefaultCredentials = true UseDefaultCredentials = true
}); });
string data = await GetWithRedirectsAsync(client, UpdateUrl).ConfigureAwait(false);
return await GetLatestVersionAsync(client, UpdateUrl).ConfigureAwait(false);
}
internal static async Task<AvailableVersionInfo> 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)); XDocument doc = XDocument.Load(new StringReader(data));
var bands = doc.Root.Elements("band").ToList(); var bands = doc.Root.Elements("band").ToList();
var currentBand = bands.FirstOrDefault(b => (string)b.Attribute("id") == band) ?? bands.First(); var currentBand = bands.FirstOrDefault(b => (string)b.Attribute("id") == band) ?? bands.First();
Version version = new Version((string)currentBand.Element("latestVersion")); 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))) string url = null;
url = null; // don't accept non-urls 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 }; LatestAvailableVersion = new AvailableVersionInfo { Version = version, DownloadUrl = url };
return LatestAvailableVersion; return LatestAvailableVersion;

2
doc/ILSpyAboutPage.txt

@ -1,6 +1,6 @@
ILSpy is the open-source .NET assembly browser and decompiler. 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 Found a bug? https://github.com/icsharpcode/ILSpy/issues/new/choose
Copyright 2011-2026 AlphaSierraPapa for the ILSpy team Copyright 2011-2026 AlphaSierraPapa for the ILSpy team

Loading…
Cancel
Save