Skip to content

Commit b5fc0d5

Browse files
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
1 parent 79b0cbb commit b5fc0d5

6 files changed

Lines changed: 144 additions & 8 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Identity Id="ebf12ca7-a1fd-4aee-a894-4a0c5682fc2f" Version="$INSERTVERSION$" Language="en-US" Publisher="SharpDevelop Team" />
55
<DisplayName>ILSpy 2022</DisplayName>
66
<Description xml:space="preserve">Integrates the ILSpy decompiler into Visual Studio.</Description>
7-
<MoreInfo>https://ilspy.net</MoreInfo>
7+
<MoreInfo>https://github.com/icsharpcode/ILSpy/</MoreInfo>
88
<License>LICENSE</License>
99
<Icon>ILSpy-Large.ico</Icon>
1010
<Tags>ILSpy;IL;decompile;decompiler;decompilation;C#;CSharp;.NET;Productivity;Open Source;Free</Tags>

ILSpy.AddIn/source.extension.vsixmanifest.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Identity Id="a9120dbe-164a-4891-842f-fb7829273838" Version="$INSERTVERSION$" Language="en-US" Publisher="ic#code" />
55
<DisplayName>ILSpy</DisplayName>
66
<Description xml:space="preserve">Integrates the ILSpy decompiler into Visual Studio.</Description>
7-
<MoreInfo>https://ilspy.net</MoreInfo>
7+
<MoreInfo>https://github.com/icsharpcode/ILSpy/</MoreInfo>
88
<License>LICENSE</License>
99
<Icon>ILSpy-Large.ico</Icon>
1010
</Metadata>

ILSpy.Tests/ILSpy.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="Analyzers\TypeUsedByAnalyzerTests.cs" />
5050
<Compile Include="CommandLineArgumentsTests.cs" />
5151
<Compile Include="ResourceReaderWriterTests.cs" />
52+
<Compile Include="UpdateServiceTests.cs" />
5253
</ItemGroup>
5354

5455
<ItemGroup>

ILSpy.Tests/UpdateServiceTests.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
using AwesomeAssertions;
8+
9+
using ICSharpCode.ILSpy.Updates;
10+
11+
using NUnit.Framework;
12+
13+
namespace ICSharpCode.ILSpy.Tests;
14+
15+
[TestFixture]
16+
public class UpdateServiceTests
17+
{
18+
[Test]
19+
public async Task GetLatestVersionAsync_UsesReleaseTag_WhenReleaseTagIsPresent()
20+
{
21+
const string xml = """
22+
<updateInfo>
23+
<band id="stable">
24+
<latestVersion>10.0.0.0</latestVersion>
25+
<releaseTag>v10.0</releaseTag>
26+
<downloadUrl>https://example.com/ignored.zip</downloadUrl>
27+
</band>
28+
</updateInfo>
29+
""";
30+
31+
using var client = new HttpClient(new StubHttpMessageHandler(xml));
32+
33+
var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml"));
34+
35+
result.Version.Should().Be(new Version(10, 0, 0, 0));
36+
result.DownloadUrl.Should().Be("https://github.com/icsharpcode/ILSpy/releases/tag/v10.0");
37+
}
38+
39+
[Test]
40+
public async Task GetLatestVersionAsync_ReturnsNullDownloadUrl_WhenReleaseTagContainsPathTraversalAttempt()
41+
{
42+
const string xml = """
43+
<updateInfo>
44+
<band id="stable">
45+
<latestVersion>10.0.0.0</latestVersion>
46+
<releaseTag>../malicious</releaseTag>
47+
<downloadUrl>https://example.com/ignored.zip</downloadUrl>
48+
</band>
49+
</updateInfo>
50+
""";
51+
52+
using var client = new HttpClient(new StubHttpMessageHandler(xml));
53+
54+
var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml"));
55+
56+
result.Version.Should().Be(new Version(10, 0, 0, 0));
57+
result.DownloadUrl.Should().BeNull();
58+
}
59+
60+
[Test]
61+
public async Task GetLatestVersionAsync_UsesDownloadUrl_WhenReleaseTagIsMissing()
62+
{
63+
const string xml = """
64+
<updateInfo>
65+
<band id="stable">
66+
<latestVersion>10.0.0.0</latestVersion>
67+
<downloadUrl>https://github.com/icsharpcode/ILSpy/releases/tag/v10.0</downloadUrl>
68+
</band>
69+
</updateInfo>
70+
""";
71+
72+
using var client = new HttpClient(new StubHttpMessageHandler(xml));
73+
74+
var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml"));
75+
76+
result.Version.Should().Be(new Version(10, 0, 0, 0));
77+
result.DownloadUrl.Should().Be("https://github.com/icsharpcode/ILSpy/releases/tag/v10.0");
78+
}
79+
80+
[Test]
81+
public async Task GetLatestVersionAsync_UsesDownloadUrl_ButFailsBecauseBaseUrlDoesntMatch()
82+
{
83+
const string xml = """
84+
<updateInfo>
85+
<band id="stable">
86+
<latestVersion>10.0.0.0</latestVersion>
87+
<downloadUrl>https://example.com/ilspy.zip</downloadUrl>
88+
</band>
89+
</updateInfo>
90+
""";
91+
92+
using var client = new HttpClient(new StubHttpMessageHandler(xml));
93+
94+
var result = await UpdateService.GetLatestVersionAsync(client, new Uri("https://example.com/updates.xml"));
95+
96+
result.Version.Should().Be(new Version(10, 0, 0, 0));
97+
result.DownloadUrl.Should().BeNull();
98+
}
99+
100+
sealed class StubHttpMessageHandler(string responseContent) : HttpMessageHandler
101+
{
102+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
103+
{
104+
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {
105+
Content = new StringContent(responseContent)
106+
});
107+
}
108+
}
109+
}

ILSpy/Updates/UpdateService.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,52 @@ namespace ICSharpCode.ILSpy.Updates
2828
{
2929
internal static class UpdateService
3030
{
31+
const string ReleaseTagBaseUrl = "https://github.com/icsharpcode/ILSpy/releases/tag/";
3132
static readonly Uri UpdateUrl = new Uri("https://icsharpcode.github.io/ILSpy/updates.xml");
3233
const string band = "stable";
3334

3435
public static AvailableVersionInfo LatestAvailableVersion { get; private set; }
3536

3637
public static async Task<AvailableVersionInfo> GetLatestVersionAsync()
3738
{
38-
var client = new HttpClient(new HttpClientHandler() {
39+
using var client = new HttpClient(new HttpClientHandler() {
3940
UseProxy = true,
4041
UseDefaultCredentials = true
4142
});
42-
string data = await GetWithRedirectsAsync(client, UpdateUrl).ConfigureAwait(false);
43+
44+
return await GetLatestVersionAsync(client, UpdateUrl).ConfigureAwait(false);
45+
}
46+
47+
internal static async Task<AvailableVersionInfo> GetLatestVersionAsync(HttpClient client, Uri updateUrl)
48+
{
49+
// Issue #3707: Remove 301 redirect logic once ilspy.net CNAME gone
50+
string data = await GetWithRedirectsAsync(client, updateUrl).ConfigureAwait(false);
4351

4452
XDocument doc = XDocument.Load(new StringReader(data));
4553
var bands = doc.Root.Elements("band").ToList();
4654
var currentBand = bands.FirstOrDefault(b => (string)b.Attribute("id") == band) ?? bands.First();
4755
Version version = new Version((string)currentBand.Element("latestVersion"));
48-
string url = (string)currentBand.Element("downloadUrl");
49-
if (!(url.StartsWith("http://", StringComparison.Ordinal) || url.StartsWith("https://", StringComparison.Ordinal)))
50-
url = null; // don't accept non-urls
56+
57+
string url = null;
58+
string releaseTag = (string)currentBand.Element("releaseTag");
59+
60+
if (releaseTag != null)
61+
{
62+
url = ReleaseTagBaseUrl + releaseTag;
63+
64+
// Prevent path traversal: normalize the URI and verify it still starts with the expected base
65+
if (!new Uri(url).AbsoluteUri.StartsWith(ReleaseTagBaseUrl, StringComparison.Ordinal))
66+
url = null;
67+
}
68+
else
69+
{
70+
// Issue #3707: Remove else branch fallback logic once releaseTag version has shipped + 6 months
71+
url = (string)currentBand.Element("downloadUrl");
72+
73+
// Prevent arbitrary URLs: verify it starts with the expected base
74+
if (!new Uri(url).AbsoluteUri.StartsWith(ReleaseTagBaseUrl, StringComparison.Ordinal))
75+
url = null;
76+
}
5177

5278
LatestAvailableVersion = new AvailableVersionInfo { Version = version, DownloadUrl = url };
5379
return LatestAvailableVersion;

doc/ILSpyAboutPage.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
ILSpy is the open-source .NET assembly browser and decompiler.
22

3-
Website: https://ilspy.net/
3+
Website: https://github.com/icsharpcode/ILSpy/
44
Found a bug? https://github.com/icsharpcode/ILSpy/issues/new/choose
55

66
Copyright 2011-2026 AlphaSierraPapa for the ILSpy team

0 commit comments

Comments
 (0)