Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ILSpy.Tests/ILSpy.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<Compile Include="Analyzers\TypeUsedByAnalyzerTests.cs" />
<Compile Include="CommandLineArgumentsTests.cs" />
<Compile Include="ResourceReaderWriterTests.cs" />
<Compile Include="UpdateServiceTests.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
89 changes: 89 additions & 0 deletions ILSpy.Tests/UpdateServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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 = """
<root>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<releaseTag>v10.0</releaseTag>
<downloadUrl>https://example.com/ignored.zip</downloadUrl>
</band>
</root>
""";

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 = """
<root>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<releaseTag>../malicious</releaseTag>
<downloadUrl>https://example.com/ignored.zip</downloadUrl>
</band>
</root>
""";

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 = """
<root>
<band id="stable">
<latestVersion>10.0.0.0</latestVersion>
<downloadUrl>https://example.com/ilspy.zip</downloadUrl>
</band>
</root>
""";

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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) {
Content = new StringContent(responseContent)
});
}
}
}
33 changes: 28 additions & 5 deletions ILSpy/Updates/UpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,49 @@ 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";

public static AvailableVersionInfo LatestAvailableVersion { get; private set; }

public static async Task<AvailableVersionInfo> 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<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));
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))
Comment thread
christophwille marked this conversation as resolved.
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)))
Comment thread
christophwille marked this conversation as resolved.
Outdated
url = null; // don't accept non-urls
}

LatestAvailableVersion = new AvailableVersionInfo { Version = version, DownloadUrl = url };
return LatestAvailableVersion;
Expand Down
Loading