Browse Source

Security fixes.

pull/1327/head
Sebastian Stehle 1 month ago
parent
commit
73922e7ae6
  1. 21
      backend/src/Squidex.Data.EntityFramework/Providers/MySql/Extensions.cs
  2. 9
      backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Extensions.cs
  3. 18
      backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Extensions.cs
  4. 14
      backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj
  5. 16
      backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj
  6. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  7. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  8. 72
      backend/src/Squidex.Infrastructure/Http/SsrfExtensions.cs
  9. 66
      backend/src/Squidex.Infrastructure/Http/SsrfHelper.cs
  10. 37
      backend/src/Squidex.Infrastructure/Http/SsrfOptions.cs
  11. 56
      backend/src/Squidex.Infrastructure/Http/SsrfProtectionHandler.cs
  12. 14
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  13. 1
      backend/src/Squidex.Shared/PermissionIds.cs
  14. 2
      backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  15. 4
      backend/src/Squidex/Config/Authentication/IdentityServices.cs
  16. 1
      backend/src/Squidex/Config/Domain/AssetServices.cs
  17. 1
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  18. 4
      backend/src/Squidex/Config/Domain/RuleServices.cs
  19. 24
      backend/src/Squidex/Squidex.csproj
  20. 1
      backend/src/Squidex/Startup.cs
  21. 18
      backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs
  22. 50
      backend/tests/Squidex.Infrastructure.Tests/Http/SsrfHelperTests.cs

21
backend/src/Squidex.Data.EntityFramework/Providers/MySql/Extensions.cs

@ -16,7 +16,8 @@ internal static class Extensions
public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath path) public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath path)
{ {
sb.Append('`'); sb.Append('`');
sb.Append(path[0]); // Escape embedded backticks so a crafted path segment cannot break out of the identifier.
sb.Append(path[0].Replace("`", "``", StringComparison.Ordinal));
sb.Append("`, "); sb.Append("`, ");
sb.AppendJsonPropertyPath(path); sb.AppendJsonPropertyPath(path);
return sb; return sb;
@ -36,7 +37,7 @@ internal static class Extensions
{ {
sb.Append('.'); sb.Append('.');
sb.Append('"'); sb.Append('"');
sb.Append(property); sb.Append(EscapeProperty(property));
sb.Append('"'); sb.Append('"');
} }
} }
@ -45,6 +46,22 @@ internal static class Extensions
return sb; return sb;
} }
// The property name is a user-controlled JSON path segment that is embedded as a double-quoted
// member inside a single-quoted SQL string literal. Escape double-quotes/backslashes at the
// JSON-path level, then backslashes/single-quotes at the MySQL string-literal level. MySQL treats
// the backslash as a string-literal escape character, so the JSON-path escapes must themselves be
// escaped again to survive string-literal parsing. This prevents SQL injection.
private static string EscapeProperty(string property)
{
return property
// JSON path escaping.
.Replace("\\", "\\\\", StringComparison.Ordinal)
.Replace("\"", "\\\"", StringComparison.Ordinal)
// MySQL string-literal escaping.
.Replace("\\", "\\\\", StringComparison.Ordinal)
.Replace("'", "''", StringComparison.Ordinal);
}
public static string JsonSubPath(this PropertyPath path) public static string JsonSubPath(this PropertyPath path)
{ {
return new StringBuilder().AppendJsonPropertyPath(path).ToString(); return new StringBuilder().AppendJsonPropertyPath(path).ToString();

9
backend/src/Squidex.Data.EntityFramework/Providers/Postgres/App/Extensions.cs

@ -16,7 +16,8 @@ public static class Extensions
public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath path, bool asString) public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath path, bool asString)
{ {
sb.Append('"'); sb.Append('"');
sb.Append(path[0]); // Escape embedded quotes so a crafted path segment cannot break out of the quoted identifier.
sb.Append(path[0].Replace("\"", "\"\"", StringComparison.Ordinal));
sb.Append('"'); sb.Append('"');
var i = 1; var i = 1;
@ -37,7 +38,11 @@ public static class Extensions
} }
else else
{ {
sb.Append($"'{property}'"); // The property name is a user-controlled JSON path segment that is embedded as a
// single-quoted string literal. Escape embedded quotes to prevent SQL injection.
sb.Append('\'');
sb.Append(property.Replace("'", "''", StringComparison.Ordinal));
sb.Append('\'');
} }
i++; i++;

18
backend/src/Squidex.Data.EntityFramework/Providers/SqlServer/Extensions.cs

@ -16,7 +16,8 @@ internal static class Extensions
public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath path) public static StringBuilder AppendJsonPath(this StringBuilder sb, PropertyPath path)
{ {
sb.Append('['); sb.Append('[');
sb.Append(path[0]); // Escape embedded closing brackets so a crafted path segment cannot break out of the identifier.
sb.Append(path[0].Replace("]", "]]", StringComparison.Ordinal));
sb.Append("], "); sb.Append("], ");
sb.AppendJsonSubPath(path); sb.AppendJsonSubPath(path);
return sb; return sb;
@ -36,7 +37,7 @@ internal static class Extensions
{ {
sb.Append('.'); sb.Append('.');
sb.Append('"'); sb.Append('"');
sb.Append(property); sb.Append(EscapeProperty(property));
sb.Append('"'); sb.Append('"');
} }
} }
@ -45,6 +46,19 @@ internal static class Extensions
return sb; return sb;
} }
// The property name is a user-controlled JSON path segment that is embedded as a double-quoted
// member inside a single-quoted SQL string literal. Escape backslashes and double-quotes at the
// JSON-path level and single-quotes at the SQL-literal level to prevent SQL injection. SQL Server
// does not treat the backslash as a string-literal escape character, so the JSON-path escapes
// reach the JSON parser verbatim.
private static string EscapeProperty(string property)
{
return property
.Replace("\\", "\\\\", StringComparison.Ordinal)
.Replace("\"", "\\\"", StringComparison.Ordinal)
.Replace("'", "''", StringComparison.Ordinal);
}
public static string JsonSubPath(this PropertyPath path) public static string JsonSubPath(this PropertyPath path)
{ {
return new StringBuilder().AppendJsonSubPath(path).ToString(); return new StringBuilder().AppendJsonSubPath(path).ToString();

14
backend/src/Squidex.Data.EntityFramework/Squidex.Data.EntityFramework.csproj

@ -43,13 +43,13 @@
<PackageReference Include="Microting.EntityFrameworkCore.MySql.Json.Microsoft" Version="10.0.6" /> <PackageReference Include="Microting.EntityFrameworkCore.MySql.Json.Microsoft" Version="10.0.6" />
<PackageReference Include="Microting.EntityFrameworkCore.MySql.NetTopologySuite" Version="10.0.6" /> <PackageReference Include="Microting.EntityFrameworkCore.MySql.NetTopologySuite" Version="10.0.6" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.EntityFramework" Version="8.0.1" /> <PackageReference Include="Squidex.AI.EntityFramework" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.EntityFramework" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.EntityFramework" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.TusAdapter" Version="8.0.3" />
<PackageReference Include="Squidex.Events.EntityFramework" Version="8.0.1" /> <PackageReference Include="Squidex.Events.EntityFramework" Version="8.0.3" />
<PackageReference Include="Squidex.Flows.EntityFramework" Version="8.0.1" /> <PackageReference Include="Squidex.Flows.EntityFramework" Version="8.0.3" />
<PackageReference Include="Squidex.Hosting" Version="8.0.1" /> <PackageReference Include="Squidex.Hosting" Version="8.0.3" />
<PackageReference Include="Squidex.Messaging.EntityFramework" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging.EntityFramework" Version="8.0.3" />
<PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="7.2.1" /> <PackageReference Include="Squidex.OpenIdDict.EntityFramework" Version="7.2.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.6.2" /> <PackageReference Include="System.ValueTuple" Version="4.6.2" />

16
backend/src/Squidex.Data.MongoDb/Squidex.Data.MongoDb.csproj

@ -20,17 +20,17 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MongoDB.Driver" Version="3.8.0" /> <PackageReference Include="MongoDB.Driver" Version="3.10.0" />
<PackageReference Include="MongoDB.Driver.Authentication.AWS" Version="3.8.0" /> <PackageReference Include="MongoDB.Driver.Authentication.AWS" Version="3.10.0" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="3.0.0" /> <PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="3.0.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI.Mongo" Version="8.0.1" /> <PackageReference Include="Squidex.AI.Mongo" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.Mongo" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.Mongo" Version="8.0.3" />
<PackageReference Include="Squidex.Events.Mongo" Version="8.0.1" /> <PackageReference Include="Squidex.Events.Mongo" Version="8.0.3" />
<PackageReference Include="Squidex.Flows.Mongo" Version="8.0.1" /> <PackageReference Include="Squidex.Flows.Mongo" Version="8.0.3" />
<PackageReference Include="Squidex.Hosting" Version="8.0.1" /> <PackageReference Include="Squidex.Hosting" Version="8.0.3" />
<PackageReference Include="Squidex.Messaging.Mongo" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging.Mongo" Version="8.0.3" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="7.2.1" /> <PackageReference Include="Squidex.OpenIddict.MongoDb" Version="7.2.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.6.2" /> <PackageReference Include="System.ValueTuple" Version="4.6.2" />

2
backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -20,7 +20,7 @@
<PackageReference Include="NetTopologySuite" Version="2.6.0" /> <PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Flows" Version="8.0.1" /> <PackageReference Include="Squidex.Flows" Version="8.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup> </ItemGroup>

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -29,8 +29,8 @@
<PackageReference Include="NJsonSchema" Version="11.6.1" /> <PackageReference Include="NJsonSchema" Version="11.6.1" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1" /> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="8.0.1" /> <PackageReference Include="Squidex.AI" Version="8.0.3" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="8.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq.Async" Version="7.0.1" /> <PackageReference Include="System.Linq.Async" Version="7.0.1" />
<PackageReference Include="ValueTaskSupplement" Version="1.1.0" /> <PackageReference Include="ValueTaskSupplement" Version="1.1.0" />

72
backend/src/Squidex.Infrastructure/Http/SsrfExtensions.cs

@ -1,72 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Squidex.Infrastructure.Http;
public static class SsrfExtensions
{
public static IHttpClientBuilder EnableSsrfProtection(this IHttpClientBuilder builder )
{
builder.Services.AddTransient<SsrfProtectionHandler>();
builder.AddHttpMessageHandler<SsrfProtectionHandler>();
builder.ConfigurePrimaryHttpMessageHandler(services =>
{
var options = services.GetService<IOptions<SsrfOptions>>()?.Value ?? new ();
return new SocketsHttpHandler
{
ConnectCallback = options.EnableDnsRebindingProtection
? CreateSecureConnectCallback(options)
: null,
AllowAutoRedirect = options.AllowAutoRedirect,
};
});
return builder;
}
private static Func<SocketsHttpConnectionContext, CancellationToken, ValueTask<Stream>> CreateSecureConnectCallback(SsrfOptions options)
{
return async (context, cancellationToken) =>
{
var host = context.DnsEndPoint.Host;
if (options.IsWhitelistedHost(host))
{
return await CreateSockedAsync(context, cancellationToken);
}
// Re-validate DNS to prevent DNS rebinding attacks
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
foreach (var address in addresses)
{
if (SsrfHelper.IsPrivateOrReservedIp(address, options.BlockedIpAddresses))
{
throw new HttpRequestException($"Connection to private IP blocked: {address}");
}
}
return await CreateSockedAsync(context, cancellationToken);
};
}
private static async Task<NetworkStream> CreateSockedAsync(SocketsHttpConnectionContext context,
CancellationToken ct)
{
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(context.DnsEndPoint, ct);
return new NetworkStream(socket, ownsSocket: true);
}
}

66
backend/src/Squidex.Infrastructure/Http/SsrfHelper.cs

@ -1,66 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Net;
using System.Net.Sockets;
#pragma warning disable SA1025 // Code should not contain multiple whitespace in a row
namespace Squidex.Infrastructure.Http;
public static class SsrfHelper
{
public static bool IsPrivateOrReservedIp(IPAddress ip, HashSet<IPAddress>? blackList)
{
if (IPAddress.IsLoopback(ip))
{
return true;
}
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
var bytes = ip.GetAddressBytes();
var isBlocked =
(bytes[0] == 10) || // 10.0.0.0/8
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) || // 172.16.0.0/12
(bytes[0] == 192 && bytes[1] == 168) || // 192.168.0.0/16
(bytes[0] == 169 && bytes[1] == 254) || // link-local
(bytes[0] == 0) || // 0.0.0.0/8
(bytes[0] >= 224 && bytes[0] <= 239) || // 224.0.0.0/4 multicast
(bytes[0] >= 240); // 240.0.0.0/4 reserved
if (isBlocked)
{
return true;
}
}
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
var bytes = ip.GetAddressBytes();
var isBlocked =
ip.IsIPv6LinkLocal || // fe80::/10
ip.IsIPv6SiteLocal || // fec0::/10 (deprecated)
ip.IsIPv6Multicast || // ff00::/8
((bytes[0] & 0xfe) == 0xfc); // fc00::/7 - Unique local
if (isBlocked)
{
return true;
}
}
if (blackList is { Count: > 0 })
{
return blackList.Contains(ip);
}
return false;
}
}

37
backend/src/Squidex.Infrastructure/Http/SsrfOptions.cs

@ -1,37 +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 sealed class SsrfOptions
{
public HashSet<string> WhitelistedHosts { get; set; } =
new HashSet<string>(
[],
StringComparer.OrdinalIgnoreCase);
public HashSet<string> AllowedSchemes { get; set; } =
new HashSet<string>(
["http", "https"],
StringComparer.OrdinalIgnoreCase);
public HashSet<IPAddress> BlockedIpAddresses { get; set; } =
new HashSet<IPAddress>(
[IPAddress.Parse("169.254.169.254")],
EqualityComparer<IPAddress>.Default);
public bool AllowAutoRedirect { get; set; }
public bool EnableDnsRebindingProtection { get; set; } = true;
public bool IsWhitelistedHost(string host)
{
return WhitelistedHosts.Contains(host) || WhitelistedHosts.Contains("*");
}
}

56
backend/src/Squidex.Infrastructure/Http/SsrfProtectionHandler.cs

@ -1,56 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Options;
namespace Squidex.Infrastructure.Http;
public class SsrfProtectionHandler(IOptions<SsrfOptions> options) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.RequestUri == null)
{
throw new HttpRequestException("Request URI is null");
}
if (!options.Value.AllowedSchemes.Contains(request.RequestUri.Scheme))
{
throw new HttpRequestException($"Scheme '{request.RequestUri.Scheme}' is not allowed");
}
var host = request.RequestUri.Host;
if (options.Value.IsWhitelistedHost(host))
{
return await base.SendAsync(request, cancellationToken);
}
try
{
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
foreach (var address in addresses)
{
if (SsrfHelper.IsPrivateOrReservedIp(address, options.Value.BlockedIpAddresses))
{
throw new HttpRequestException($"Request blocked: '{host}' resolves to private IP {address}");
}
}
}
catch (SocketException ex)
{
throw new HttpRequestException($"DNS resolution failed for '{host}'", ex);
}
return await base.SendAsync(request, cancellationToken);
}
}

14
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,13 +24,13 @@
<PackageReference Include="NodaTime" Version="3.3.1" /> <PackageReference Include="NodaTime" Version="3.3.1" />
<PackageReference Include="OpenTelemetry.Api" Version="1.15.3" /> <PackageReference Include="OpenTelemetry.Api" Version="1.15.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="8.0.1" /> <PackageReference Include="Squidex.Assets" Version="8.0.3" />
<PackageReference Include="Squidex.Caching" Version="8.0.1" /> <PackageReference Include="Squidex.Caching" Version="8.0.3" />
<PackageReference Include="Squidex.Events" Version="8.0.1" /> <PackageReference Include="Squidex.Events" Version="8.0.3" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="8.0.1" /> <PackageReference Include="Squidex.Hosting.Abstractions" Version="8.0.3" />
<PackageReference Include="Squidex.Log" Version="8.0.1" /> <PackageReference Include="Squidex.Log" Version="8.0.3" />
<PackageReference Include="Squidex.Messaging" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging" Version="8.0.3" />
<PackageReference Include="Squidex.Text" Version="8.0.1" /> <PackageReference Include="Squidex.Text" Version="8.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Linq.Async" Version="7.0.1" /> <PackageReference Include="System.Linq.Async" Version="7.0.1" />

1
backend/src/Squidex.Shared/PermissionIds.cs

@ -34,6 +34,7 @@ public static class PermissionIds
public const string AdminUsers = "squidex.admin.users"; public const string AdminUsers = "squidex.admin.users";
public const string AdminUsersRead = "squidex.admin.users.read"; public const string AdminUsersRead = "squidex.admin.users.read";
public const string AdminUsersCreate = "squidex.admin.users.create"; public const string AdminUsersCreate = "squidex.admin.users.create";
public const string AdminUsersDelete = "squidex.admin.users.delete";
public const string AdminUsersUpdate = "squidex.admin.users.update"; public const string AdminUsersUpdate = "squidex.admin.users.update";
public const string AdminUsersUnlock = "squidex.admin.users.unlock"; public const string AdminUsersUnlock = "squidex.admin.users.unlock";
public const string AdminUsersLock = "squidex.admin.users.lock"; public const string AdminUsersLock = "squidex.admin.users.lock";

2
backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -167,7 +167,7 @@ public sealed class UserManagementController(ICommandBus commandBus, IUserServic
[HttpDelete] [HttpDelete]
[Route("user-management/{id}/")] [Route("user-management/{id}/")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ApiPermission(PermissionIds.AdminUsersUnlock)] [ApiPermission(PermissionIds.AdminUsersDelete)]
public async Task<IActionResult> DeleteUser(string id) public async Task<IActionResult> DeleteUser(string id)
{ {
if (this.IsUser(id)) if (this.IsUser(id))

4
backend/src/Squidex/Config/Authentication/IdentityServices.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Hosting.Ssrf;
using Squidex.Shared.Users; using Squidex.Shared.Users;
namespace Squidex.Config.Authentication; namespace Squidex.Config.Authentication;
@ -17,7 +18,8 @@ public static class IdentityServices
services.Configure<MyIdentityOptions>(config, services.Configure<MyIdentityOptions>(config,
"identity"); "identity");
services.AddHttpClient("Users"); services.AddHttpClient("Users")
.EnableSsrfProtection();
services.AddSingletonAs<DefaultUserResolver>() services.AddSingletonAs<DefaultUserResolver>()
.AsOptional<IUserResolver>(); .AsOptional<IUserResolver>();

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

@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Entities.Assets.Queries;
using Squidex.Domain.Apps.Entities.Assets.Queries.Steps; using Squidex.Domain.Apps.Entities.Assets.Queries.Steps;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
using Squidex.Domain.Apps.Entities.Search; using Squidex.Domain.Apps.Entities.Search;
using Squidex.Hosting.Ssrf;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http; using Squidex.Infrastructure.Http;

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

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.Templates.Extensions; using Squidex.Domain.Apps.Core.Templates.Extensions;
using Squidex.Domain.Apps.Entities.Contents.Counter; using Squidex.Domain.Apps.Entities.Contents.Counter;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
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.Http;

4
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -22,7 +22,6 @@ using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Flows.Internal.Execution; using Squidex.Flows.Internal.Execution;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Http;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Config.Domain; namespace Squidex.Config.Domain;
@ -34,9 +33,6 @@ public static class RuleServices
services.Configure<RulesOptions>(config, services.Configure<RulesOptions>(config,
"rules"); "rules");
services.Configure<SsrfOptions>(config,
"ssrf");
services.AddSingletonAs<EventEnricher>() services.AddSingletonAs<EventEnricher>()
.As<IEventEnricher>(); .As<IEventEnricher>();

24
backend/src/Squidex/Squidex.csproj

@ -57,16 +57,16 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" /> <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.5.6" PrivateAssets="all" /> <PackageReference Include="ReportGenerator" Version="5.5.6" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.Azure" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.GoogleCloud" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.FTP" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.FTP" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.ImageSharp" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.S3" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.S3" Version="8.0.3" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.TusAdapter" Version="8.0.3" />
<PackageReference Include="Squidex.ClientLibrary" Version="22.2.0" /> <PackageReference Include="Squidex.ClientLibrary" Version="22.2.0" />
<PackageReference Include="Squidex.Hosting" Version="8.0.1" /> <PackageReference Include="Squidex.Hosting" Version="8.0.3" />
<PackageReference Include="Squidex.Messaging.All" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging.All" Version="8.0.3" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging.Subscriptions" Version="8.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.6.0" /> <PackageReference Include="YDotNet" Version="0.6.0" />
<PackageReference Include="YDotNet.Native" Version="0.6.0" /> <PackageReference Include="YDotNet.Native" Version="0.6.0" />
@ -80,12 +80,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(IncludeMagick)' == 'true'"> <ItemGroup Condition="'$(IncludeMagick)' == 'true'">
<PackageReference Include="Squidex.Assets.ImageMagick" Version="8.0.1" /> <PackageReference Include="Squidex.Assets.ImageMagick" Version="8.0.3" />
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.12.0" /> <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.16.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(IncludeKafka)' == 'true'"> <ItemGroup Condition="'$(IncludeKafka)' == 'true'">
<PackageReference Include="Squidex.Messaging.Kafka" Version="8.0.1" /> <PackageReference Include="Squidex.Messaging.Kafka" Version="8.0.3" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

1
backend/src/Squidex/Startup.cs

@ -27,6 +27,7 @@ public sealed class Startup(IConfiguration config)
services.AddHealthChecks(); services.AddHealthChecks();
services.AddDefaultWebServices(config); services.AddDefaultWebServices(config);
services.AddDefaultForwardRules(); services.AddDefaultForwardRules();
services.AddSsrfProtectedHttpClient(config);
// They must be called in this order. // They must be called in this order.
services.AddSquidexMvcWithPlugins(config); services.AddSquidexMvcWithPlugins(config);

18
backend/tests/Squidex.Data.Tests/EntityFramework/Infrastructure/Queries/EFQueryTests.cs

@ -890,6 +890,24 @@ public abstract class EFQueryTests<TContext>(ISqlFixture<TContext> fixture)
Assert.Equal(AllExept(7), actual.Order().ToArray()); Assert.Equal(AllExept(7), actual.Order().ToArray());
} }
[Theory]
[InlineData("x' OR '1'='1")]
[InlineData("x\" OR \"1\"=\"1")]
[InlineData("x') OR (1=1) --")]
[InlineData("x\\")]
public async Task Should_not_allow_sql_injection_through_json_path(string malicious)
{
// A crafted JSON path segment must be treated as a literal (non-matching) key and must not
// break out of the generated SQL. If escaping failed, an OR-based payload would either raise
// a SQL syntax error or leak every row instead of returning nothing.
var actual = await QueryAsync(new ClrQuery
{
Filter = ClrFilter.Eq($"Json.mixed.{malicious}", "value"),
});
Assert.Empty(actual);
}
[Fact] [Fact]
public async Task Should_filter_by_string_contains_in_json() public async Task Should_filter_by_string_contains_in_json()
{ {

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

@ -123,6 +123,56 @@ public class SsrfHelperTests
Assert.True(result); 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] [Theory]
[InlineData("8.8.8.8")] [InlineData("8.8.8.8")]
[InlineData("1.1.1.1")] [InlineData("1.1.1.1")]

Loading…
Cancel
Save