From 13e9f1ac2a0d1b7f98cdb8c4a888176aa3e9ceee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Mon, 12 Jul 2021 13:40:23 +0200 Subject: [PATCH] Closes #2371 (#2372) * Closes #2371 * Change the default to no known networks * Address @Vital7 note * Handle both IPv4 and IPv6 when mapped This follows ASP.NET Core logic * Refactor forwarded headers usage --- .../ApiAuthenticationMiddleware.cs | 39 +++++++++++++++---- ArchiSteamFarm/IPC/Startup.cs | 9 +++-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index c994cbc8977bf..205e898ac26b8 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -30,7 +30,9 @@ using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Storage; using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace ArchiSteamFarm.IPC.Integration { @@ -46,11 +48,18 @@ internal sealed class ApiAuthenticationMiddleware { private static Timer? ClearFailedAuthorizationsTimer; + private readonly ForwardedHeadersOptions ForwardedHeadersOptions; private readonly RequestDelegate Next; - public ApiAuthenticationMiddleware(RequestDelegate next) { + public ApiAuthenticationMiddleware(RequestDelegate next, IOptions forwardedHeadersOptions) { Next = next ?? throw new ArgumentNullException(nameof(next)); + if (forwardedHeadersOptions == null) { + throw new ArgumentNullException(nameof(forwardedHeadersOptions)); + } + + ForwardedHeadersOptions = forwardedHeadersOptions.Value ?? throw new InvalidOperationException(nameof(forwardedHeadersOptions)); + lock (FailedAuthorizations) { ClearFailedAuthorizationsTimer ??= new Timer( _ => FailedAuthorizations.Clear(), @@ -78,7 +87,7 @@ public async Task InvokeAsync(HttpContext context) { await Next(context).ConfigureAwait(false); } - private static async Task GetAuthenticationStatus(HttpContext context) { + private async Task GetAuthenticationStatus(HttpContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } @@ -87,16 +96,32 @@ private static async Task GetAuthenticationStatus(HttpContext co throw new InvalidOperationException(nameof(ClearFailedAuthorizationsTimer)); } + IPAddress? clientIP = context.Connection.RemoteIpAddress; + + if (clientIP == null) { + throw new InvalidOperationException(nameof(clientIP)); + } + string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; if (string.IsNullOrEmpty(ipcPassword)) { - return HttpStatusCode.OK; - } + if (IPAddress.IsLoopback(clientIP)) { + return HttpStatusCode.OK; + } - IPAddress? clientIP = context.Connection.RemoteIpAddress; + if (ForwardedHeadersOptions.KnownNetworks.Count == 0) { + return HttpStatusCode.Forbidden; + } - if (clientIP == null) { - throw new InvalidOperationException(nameof(clientIP)); + if (clientIP.IsIPv4MappedToIPv6) { + IPAddress mappedClientIP = clientIP.MapToIPv4(); + + if (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(mappedClientIP))) { + return HttpStatusCode.OK; + } + } + + return ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; } if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index 0f377183cc8e5..c918e8bc7c746 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -148,12 +148,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); #endif + // We want to protect our API with IPCPassword and additional security, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist + app.UseWhen(context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseMiddleware()); + string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; if (!string.IsNullOrEmpty(ipcPassword)) { - // We want to protect our API with IPCPassword, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist - app.UseWhen(context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseMiddleware()); - // We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API, this should be called before response compression, but can't be due to how our flow works // We apply CORS policy only with IPCPassword set as an extra authentication measure app.UseCors(); @@ -197,7 +197,8 @@ public void ConfigureServices(IServiceCollection services) { HashSet? knownNetworks = null; if (knownNetworksTexts?.Count > 0) { - knownNetworks = new HashSet(knownNetworksTexts.Count); + // Use specified known networks + knownNetworks = new HashSet(); foreach (string knownNetworkText in knownNetworksTexts) { string[] addressParts = knownNetworkText.Split('/', StringSplitOptions.RemoveEmptyEntries);