Browse Source

Fix build.

pull/1327/head
Sebastian Stehle 1 month ago
parent
commit
9ffc149d8f
  1. 4
      backend/extensions/Squidex.Extensions/LogMessages.cs
  2. 13
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplatesClient.cs
  3. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  4. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  6. 1
      backend/src/Squidex/Config/Domain/AssetServices.cs
  7. 1
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  8. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  9. 243
      backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs
  10. 131
      backend/tests/Squidex.Infrastructure.Tests/Http/SsrfProtectionHandlerTests.cs

4
backend/extensions/Squidex.Extensions/LogMessages.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -12,7 +12,9 @@ namespace Squidex.Extensions;
internal static partial class LogMessages internal static partial class LogMessages
{ {
[LoggerMessage(Level = LogLevel.Warning, Message = "Kafka error with {code} and {reason}.")] [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); 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.")] [LoggerMessage(Level = LogLevel.Error, Message = "Failed to enrich asset.")]
public static partial void LogFailedToEnrichAsset(ILogger logger, Exception exception); public static partial void LogFailedToEnrichAsset(ILogger logger, Exception exception);

13
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<TemplatesOptions> options) public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory, IOptions<TemplatesOptions> options)
{ {
private static readonly Regex RegexTemplate = BuildTemplateRegex();
private readonly TemplatesOptions options = options.Value; private readonly TemplatesOptions options = options.Value;
public async Task<string?> GetRepositoryUrl(string name, public async Task<string?> GetRepositoryUrl(string name,
@ -31,7 +30,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory
var text = await httpClient.GetStringAsync(url, ct); var text = await httpClient.GetStringAsync(url, ct);
foreach (var match in RegexTemplate.Matches(text).OfType<Match>()) foreach (var match in TemplateRegex.Matches(text).OfType<Match>())
{ {
var currentName = match.Groups["Name"].Value; var currentName = match.Groups["Name"].Value;
@ -58,7 +57,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory
var text = await httpClient.GetStringAsync(url, ct); var text = await httpClient.GetStringAsync(url, ct);
foreach (var match in RegexTemplate.Matches(text).OfType<Match>()) foreach (var match in TemplateRegex.Matches(text).OfType<Match>())
{ {
var templateName = match.Groups["Name"].Value; var templateName = match.Groups["Name"].Value;
var templateTitle = match.Groups["Title"].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); var text = await response.Content.ReadAsStringAsync(ct);
string? logo = null; string? logo = null;
text = BuildLogoRegex().Replace(text, match => text = LogoRegex.Replace(text, match =>
{ {
var imageRelative = new Uri(match.Groups["Url"].Value, UriKind.Relative); var imageRelative = new Uri(match.Groups["Url"].Value, UriKind.Relative);
var imageAbsolute = new Uri(url, imageRelative); var imageAbsolute = new Uri(url, imageRelative);
@ -161,7 +160,7 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory
{ {
if (inline is LiteralInline literal) 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) if (inline is ContainerInline container)
@ -182,8 +181,8 @@ public sealed partial class TemplatesClient(IHttpClientFactory httpClientFactory
} }
[GeneratedRegex("\\* \\[(?<Title>.*)\\]\\((?<Name>.*)\\/README\\.md\\): (?<Description>.*)", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] [GeneratedRegex("\\* \\[(?<Title>.*)\\]\\((?<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)] [GeneratedRegex("Logo: \\[Logo\\]\\((?<Url>(.*))\\)", RegexOptions.ExplicitCapture | RegexOptions.Compiled)]
private static partial Regex BuildLogoRegex(); private static partial Regex LogoRegex { get; }
} }

3
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.Messaging.Subscriptions;
using Squidex.Shared; 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; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets;
internal static class AssetActions internal static class AssetActions

3
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.Messaging.Subscriptions;
using Squidex.Shared; 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; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
internal static class ContentActions internal static class ContentActions

2
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.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
#pragma warning disable MA0005 // Use Array.Empty<T>()
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
internal static class ContentFields internal static class ContentFields

1
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.Domain.Apps.Entities.Search;
using Squidex.Hosting.Ssrf; using Squidex.Hosting.Ssrf;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http;
namespace Squidex.Config.Domain; namespace Squidex.Config.Domain;

1
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -22,7 +22,6 @@ using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Hosting.Ssrf; using Squidex.Hosting.Ssrf;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.Http;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -99,7 +99,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
invalid(() invalid(()
"; ";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync([], script));
} }
[Fact] [Fact]
@ -109,7 +109,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
throw 'Error'; throw 'Error';
"; ";
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync([], script));
} }
[Fact] [Fact]
@ -179,7 +179,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
throw 'Error'; throw 'Error';
"; ";
await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync(new DataScriptVars(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.TransformAsync([], script));
} }
[Fact] [Fact]

243
backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs

@ -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);
}
}

131
backend/tests/Squidex.Infrastructure.Tests/Http/SsrfProtectionHandlerTests.cs

@ -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));
}
}
Loading…
Cancel
Save