From 9ffc149d8fae889d666f01a4fa45b46f63da14b2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Thu, 30 Jul 2026 14:37:06 +0200 Subject: [PATCH] Fix build. --- .../Squidex.Extensions/LogMessages.cs | 4 +- .../Apps/Templates/TemplatesClient.cs | 13 +- .../GraphQL/Types/Assets/AssetActions.cs | 3 + .../GraphQL/Types/Contents/ContentActions.cs | 3 + .../GraphQL/Types/Contents/ContentFields.cs | 2 + .../Squidex/Config/Domain/AssetServices.cs | 1 - .../Config/Domain/InfrastructureServices.cs | 1 - .../Scripting/JintScriptEngineTests.cs | 6 +- .../Http/SsrfHelperTests.cs | 243 ------------------ .../Http/SsrfProtectionHandlerTests.cs | 131 ---------- 10 files changed, 20 insertions(+), 387 deletions(-) delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Http/SsrfProtectionHandlerTests.cs diff --git a/backend/extensions/Squidex.Extensions/LogMessages.cs b/backend/extensions/Squidex.Extensions/LogMessages.cs index 8d43ba40e..ff0171037 100644 --- a/backend/extensions/Squidex.Extensions/LogMessages.cs +++ b/backend/extensions/Squidex.Extensions/LogMessages.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -12,7 +12,9 @@ namespace Squidex.Extensions; internal static partial class LogMessages { [LoggerMessage(Level = LogLevel.Warning, Message = "Kafka error with {code} and {reason}.")] +#pragma warning disable LOGGEN036 // A value being logged doesn't have an effective way to be converted into a string public static partial void LogKafkaError(ILogger logger, object code, string reason); +#pragma warning restore LOGGEN036 // A value being logged doesn't have an effective way to be converted into a string [LoggerMessage(Level = LogLevel.Error, Message = "Failed to enrich asset.")] public static partial void LogFailedToEnrichAsset(ILogger logger, Exception exception); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs index 33e4be47c..a472ef80d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs @@ -17,7 +17,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates; public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory, IOptions options) { - private static readonly Regex RegexTemplate = BuildTemplateRegex(); private readonly TemplatesOptions options = options.Value; public async Task GetRepositoryUrl(string name, @@ -31,7 +30,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory var text = await httpClient.GetStringAsync(url, ct); - foreach (var match in RegexTemplate.Matches(text).OfType()) + foreach (var match in TemplateRegex.Matches(text).OfType()) { var currentName = match.Groups["Name"].Value; @@ -58,7 +57,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory var text = await httpClient.GetStringAsync(url, ct); - foreach (var match in RegexTemplate.Matches(text).OfType()) + foreach (var match in TemplateRegex.Matches(text).OfType()) { var templateName = match.Groups["Name"].Value; var templateTitle = match.Groups["Title"].Value; @@ -109,7 +108,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory var text = await response.Content.ReadAsStringAsync(ct); string? logo = null; - text = BuildLogoRegex().Replace(text, match => + text = LogoRegex.Replace(text, match => { var imageRelative = new Uri(match.Groups["Url"].Value, UriKind.Relative); var imageAbsolute = new Uri(url, imageRelative); @@ -161,7 +160,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory { if (inline is LiteralInline literal) { - return literal.Content.AsSpan().Trim().Equals("Usage", StringComparison.Ordinal); + return literal.Content.AsSpan().Trim() is "Usage"; } if (inline is ContainerInline container) @@ -182,8 +181,8 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory } [GeneratedRegex("\\* \\[(?.*)\\]\\((?<Name>.*)\\/README\\.md\\): (?<Description>.*)", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] - private static partial Regex BuildTemplateRegex(); + private static partial Regex TemplateRegex { get; } [GeneratedRegex("Logo: \\[Logo\\]\\((?<Url>(.*))\\)", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] - private static partial Regex BuildLogoRegex(); + private static partial Regex LogoRegex { get; } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index 351e761b3..2bde89881 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -18,6 +18,9 @@ using Squidex.Infrastructure.Translations; using Squidex.Messaging.Subscriptions; using Squidex.Shared; +#pragma warning disable MA0005 // Use Array.Empty<T>() +#pragma warning disable CA1825 // Avoid zero-length array allocations + namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets; internal static class AssetActions diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index f20bc699d..597d8537e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -20,6 +20,9 @@ using Squidex.Infrastructure.Translations; using Squidex.Messaging.Subscriptions; using Squidex.Shared; +#pragma warning disable MA0005 // Use Array.Empty<T>() +#pragma warning disable CA1825 // Avoid zero-length array allocations + namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; internal static class ContentActions diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs index 56b29e239..9e787b4b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs @@ -14,6 +14,8 @@ using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Infrastructure.Json.Objects; +#pragma warning disable MA0005 // Use Array.Empty<T>() + namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; internal static class ContentFields diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index f7102b4cd..090c90128 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -13,7 +13,6 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Hosting.Ssrf; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Http; namespace Squidex.Config.Domain; diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index f88678654..c256f7fe2 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -22,7 +22,6 @@ using Squidex.Domain.Apps.Entities.Tags; using Squidex.Hosting.Ssrf; using Squidex.Infrastructure; using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index 80395b4e5..fcc924cd2 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -99,7 +99,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture> invalid(() "; - await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); + await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync([], script)); } [Fact] @@ -109,7 +109,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture> throw 'Error'; "; - await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); + await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync([], script)); } [Fact] @@ -179,7 +179,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture> throw 'Error'; "; - await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(new DataScriptVars(), script)); + await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync([], script)); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs deleted file mode 100644 index e929cf1ae..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs +++ /dev/null @@ -1,243 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; - -namespace Squidex.Infrastructure.Http; - -public class SsrfHelperTests -{ - [Theory] - [InlineData("127.0.0.1")] - [InlineData("::1")] - public void Should_block_loopback_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("10.0.0.1")] - [InlineData("10.255.255.255")] - [InlineData("172.16.0.1")] - [InlineData("172.31.255.255")] - [InlineData("192.168.0.1")] - [InlineData("192.168.255.255")] - public void Should_block_private_ipv4_ranges(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("169.254.0.1")] - [InlineData("169.254.169.254")] - public void Should_block_link_local_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("0.0.0.0")] - [InlineData("0.255.255.255")] - public void Should_block_current_network_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("224.0.0.1")] - [InlineData("239.255.255.255")] - public void Should_block_multicast_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("240.0.0.1")] - [InlineData("255.255.255.255")] - public void Should_block_reserved_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("fe80::1")] - [InlineData("fec0::1")] - public void Should_block_ipv6_link_local_and_site_local(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("fc00::1")] - [InlineData("fd00::1")] - public void Should_block_ipv6_unique_local_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("ff00::1")] - [InlineData("ff02::1")] - public void Should_block_ipv6_multicast_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Theory] - [InlineData("::ffff:127.0.0.1")] - [InlineData("::ffff:10.0.0.1")] - [InlineData("::ffff:172.16.0.1")] - [InlineData("::ffff:192.168.0.1")] - [InlineData("::ffff:169.254.169.254")] - [InlineData("::ffff:0.0.0.0")] - public void Should_block_ipv4_mapped_ipv6_private_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.True(result); - } - - [Fact] - public void Should_block_ipv4_mapped_ipv6_of_blacklisted_ipv4() - { - var address = IPAddress.Parse("::ffff:169.254.169.254"); - var blacklist = new HashSet<IPAddress> { IPAddress.Parse("169.254.169.254") }; - - var result = SsrfHelper.IsPrivateOrReservedIp(address, blacklist); - - Assert.True(result); - } - - [Fact] - public void Should_block_ipv4_of_blacklisted_ipv4_mapped_ipv6() - { - var address = IPAddress.Parse("1.2.3.4"); - var blacklist = new HashSet<IPAddress> { IPAddress.Parse("::ffff:1.2.3.4") }; - - var result = SsrfHelper.IsPrivateOrReservedIp(address, blacklist); - - Assert.True(result); - } - - [Theory] - [InlineData("::ffff:8.8.8.8")] - [InlineData("::ffff:1.1.1.1")] - public void Should_allow_ipv4_mapped_ipv6_public_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.False(result); - } - - [Theory] - [InlineData("8.8.8.8")] - [InlineData("1.1.1.1")] - [InlineData("203.0.113.1")] - public void Should_allow_public_ipv4_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.False(result); - } - - [Theory] - [InlineData("2001:4860:4860::8888")] - [InlineData("2606:4700:4700::1111")] - public void Should_allow_public_ipv6_addresses(string ip) - { - var address = IPAddress.Parse(ip); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.False(result); - } - - [Fact] - public void Should_block_custom_blacklisted_ip() - { - var address = IPAddress.Parse("1.2.3.4"); - var blacklist = new HashSet<IPAddress> { IPAddress.Parse("1.2.3.4") }; - - var result = SsrfHelper.IsPrivateOrReservedIp(address, blacklist); - - Assert.True(result); - } - - [Fact] - public void Should_allow_ip_not_in_blacklist() - { - var address = IPAddress.Parse("8.8.8.8"); - var blacklist = new HashSet<IPAddress> { IPAddress.Parse("1.2.3.4") }; - - var result = SsrfHelper.IsPrivateOrReservedIp(address, blacklist); - - Assert.False(result); - } - - [Fact] - public void Should_handle_null_blacklist() - { - var address = IPAddress.Parse("8.8.8.8"); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, null); - - Assert.False(result); - } - - [Fact] - public void Should_handle_empty_blacklist() - { - var address = IPAddress.Parse("8.8.8.8"); - var blacklist = new HashSet<IPAddress>(); - - var result = SsrfHelper.IsPrivateOrReservedIp(address, blacklist); - - Assert.False(result); - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Http/SsrfProtectionHandlerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Http/SsrfProtectionHandlerTests.cs deleted file mode 100644 index 85cea16d0..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Http/SsrfProtectionHandlerTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using Microsoft.Extensions.Options; - -namespace Squidex.Infrastructure.Http; - -public class SsrfProtectionHandlerTests -{ - private readonly SsrfCustomHandler sut; - private readonly SsrfOptions options = new (); - - private sealed class SsrfCustomHandler(IOptions<SsrfOptions> options) : SsrfProtectionHandler(options) - { - public new async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) - { - return await base.SendAsync(request, cancellationToken); - } - } - - private sealed class TestHttpMessageHandler : HttpMessageHandler - { - protected override Task<HttpResponseMessage> SendAsync( - HttpRequestMessage request, - CancellationToken cancellationToken) - { - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); - } - } - - public SsrfProtectionHandlerTests() - { - sut = new SsrfCustomHandler(Options.Create(options)) - { - InnerHandler = new TestHttpMessageHandler(), - }; - } - - [Theory] - [InlineData("http://example.com")] - [InlineData("https://example.com")] - public async Task Should_allow_http_and_https_schemes(string url) - { - var request = new HttpRequestMessage(HttpMethod.Get, url); - - await sut.SendAsync(request, CancellationToken.None); - } - - [Theory] - [InlineData("ftp://example.com")] - [InlineData("file:///etc/passwd")] - public async Task Should_block_non_http_schemes(string url) - { - var request = new HttpRequestMessage(HttpMethod.Get, url); - - await Assert.ThrowsAsync<HttpRequestException>(() => - sut.SendAsync(request, CancellationToken.None)); - } - - [Fact] - public async Task Should_throw_exception_if_request_uri_is_null() - { - var request = new HttpRequestMessage(HttpMethod.Get, (Uri?)null); - - await Assert.ThrowsAsync<HttpRequestException>(() => - sut.SendAsync(request, CancellationToken.None)); - } - - [Fact] - public async Task Should_block_request_to_localhost() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost"); - - await Assert.ThrowsAsync<HttpRequestException>(() => - sut.SendAsync(request, CancellationToken.None)); - } - - [Fact] - public async Task Should_block_request_to_loopback_ip() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://127.0.0.1"); - - await Assert.ThrowsAsync<HttpRequestException>(() => - sut.SendAsync(request, CancellationToken.None)); - } - - [Fact] - public async Task Should_not_block_request_to_localhost_if_whitelisted() - { - options.WhitelistedHosts.Add("localhost"); - - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost"); - - await sut.SendAsync(request, CancellationToken.None); - } - - [Fact] - public async Task Should_not_block_request_to_localhost_if_all_hosts_are_whitelisted() - { - options.WhitelistedHosts.Add("*"); - - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost"); - - await sut.SendAsync(request, CancellationToken.None); - } - - [Fact] - public async Task Should_allow_custom_scheme_when_configured() - { - options.AllowedSchemes.Add("custom"); - - var request = new HttpRequestMessage(HttpMethod.Get, "custom://example.com"); - - await sut.SendAsync(request, CancellationToken.None); - } - - [Fact] - public async Task Should_throw_exception_on_dns_resolution_failure() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://invalid.domain.that.does.not.exist.local"); - - await Assert.ThrowsAsync<HttpRequestException>(() => - sut.SendAsync(request, CancellationToken.None)); - } -}