Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817
This commit is contained in:
commit
ad8ad252a9
825 files changed
+124216
No files matched your search
@@ -0,0 +1,102 @@
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.Api.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Proves the reverse-proxy trust boundary documented in
|
||||
/// docs/23-deployment-unraid.md "Reverse proxy" and EVIDENCE.md's "Production
|
||||
/// configuration" item: forwarded headers (X-Forwarded-Proto) are only honored when
|
||||
/// they arrive from an explicitly configured KnowledgeHub:TrustedProxies entry, never
|
||||
/// by default and never from an untrusted source. The CSRF cookie's
|
||||
/// CookieSecurePolicy.SameAsRequest makes the resulting "Secure" attribute a direct,
|
||||
/// observable proxy for whether ASP.NET Core actually treated the request as HTTPS.
|
||||
/// </summary>
|
||||
public sealed class ForwardedHeadersTrustTests
|
||||
{
|
||||
private const string SimulatedRemoteIp = "203.0.113.7";
|
||||
|
||||
// WebApplicationFactory.CreateClient() defaults its base address to https://localhost,
|
||||
// which alone already makes TestServer report Request.IsHttps == true — independent of
|
||||
// any forwarded header. Every test below forces an http:// base address instead, so the
|
||||
// "Secure" cookie attribute genuinely reflects whether the ForwardedHeaders middleware
|
||||
// honored (or correctly ignored) X-Forwarded-Proto, not this client default.
|
||||
private static readonly WebApplicationFactoryClientOptions HttpClientOptions = new()
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost"),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task An_untrusted_X_Forwarded_Proto_header_is_ignored_by_default()
|
||||
{
|
||||
using var factory = new KnowledgeHubApiFactory();
|
||||
using var client = factory.CreateClient(HttpClientOptions);
|
||||
|
||||
var csrfCookie = await GetCsrfCookieAsync(client, SimulatedRemoteIp);
|
||||
|
||||
Assert.DoesNotContain("Secure", csrfCookie, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_X_Forwarded_Proto_header_from_a_configured_trusted_proxy_is_honored()
|
||||
{
|
||||
using var factory = new KnowledgeHubApiFactory().WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["KnowledgeHub:TrustedProxies"] = $"{SimulatedRemoteIp};127.0.0.1",
|
||||
});
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient(HttpClientOptions);
|
||||
|
||||
var csrfCookie = await GetCsrfCookieAsync(client, SimulatedRemoteIp);
|
||||
|
||||
Assert.Contains("Secure", csrfCookie, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_trusted_proxy_list_that_does_not_include_the_caller_still_ignores_the_header()
|
||||
{
|
||||
using var factory = new KnowledgeHubApiFactory().WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
// A real trusted proxy is configured, but it is not the address this
|
||||
// request actually arrives from — the header must still be ignored.
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["KnowledgeHub:TrustedProxies"] = "198.51.100.9",
|
||||
});
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient(HttpClientOptions);
|
||||
|
||||
var csrfCookie = await GetCsrfCookieAsync(client, SimulatedRemoteIp);
|
||||
|
||||
Assert.DoesNotContain("Secure", csrfCookie, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static async Task<string> GetCsrfCookieAsync(HttpClient client, string simulatedRemoteIp)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/auth/csrf");
|
||||
// TestServer never simulates a real socket, so Connection.RemoteIpAddress is null
|
||||
// unless something sets it. Program.cs sets it from this header in the Test
|
||||
// environment only (never reachable in Production), giving the KnownProxies
|
||||
// trust check a deterministic "direct caller" address — the same role a real
|
||||
// kernel-level TCP accept plays in production.
|
||||
request.Headers.Add("X-Test-Remote-Ip", simulatedRemoteIp);
|
||||
request.Headers.Add("X-Forwarded-Proto", "https");
|
||||
request.Headers.Add("X-Forwarded-For", "198.51.100.50");
|
||||
|
||||
using var response = await client.SendAsync(request);
|
||||
|
||||
var csrfCookie = response.Headers.TryGetValues("Set-Cookie", out var values)
|
||||
? values.FirstOrDefault(v => v.StartsWith("ikh_csrf=", StringComparison.Ordinal))
|
||||
: null;
|
||||
Assert.NotNull(csrfCookie);
|
||||
return csrfCookie;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user