dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.02k forks source link

OpaqueRedirection doesn't respect HTTP/HTTPS scheme behind a load balancer. #57650

Closed brad-technologik closed 1 month ago

brad-technologik commented 2 months ago

Is there an existing issue for this?

Describe the bug

I run my blazor hybrid app behind a load balancer. However, this causes NavigationManager.NavigateTo to break, since it tries to redirect to an HTTP scheme when my app is accessed through HTTPS. Even after following instructions at Configure ASP.NET Core to work with proxy servers and load balancers, the http scheme is still not respected.

During redirection, the browser emits an error and does not redirect:

Mixed Content: The page at 'https://example.com/kanban' was loaded over HTTPS, but requested an insecure resource 'https://example.com/kanban/media'. This request has been blocked; the content must be served over HTTPS.

Refreshing the page redirect correctly since it doesn't use OpaqueRedirection.

Expected Behavior

The opaque redirector should honor the HTTPS/HTTPS scheme based on forwarded headers from a load balancer.

Steps To Reproduce

Clone my repo here: https://github.com/TrieBr/redirect-bug

  1. Generate and trust certificates for reverse proxy:
    cd nginx
    dotnet dev-certs https -ep ./OpaqueRedirectionBug.pfx -p password --trust
    openssl pkcs12 -in OpaqueRedirectionBug.pfx -nocerts -out OpaqueRedirectionBug.key

    Enter 'password' for all 3 prompts.

    openssl pkcs12 -in OpaqueRedirectionBug.pfx -clcerts -nokeys -out OpaqueRedirectionBug.crt

    Enter 'password'.

openssl rsa -in OpaqueRedirectionBug.key -out OpaqueRedirectionBug.key

Enter 'password'.

  1. CD back into the root folder (cd ../), and run docker-compose up.

  2. visit https://localhost on your browser.

  3. Open developer tools.

  4. Then click "Weather" on the left navigation.

  5. Observe in the "network" tab, the fetch of something like https://localhost/_framework/opaque-redirect?url=...

  6. Observe that the returned header Location uses http and not https.

On localhost, this isn't an issue, but on a real domain, the browser will throw an error: This request has been blocked; the content must be served over HTTPS

The returned Location header should use the same http scheme in the request, honoring load balancer forwarded schemes.

Exceptions (if any)

No response

.NET Version

8.0.100

Anything else?

.NET SDK: Version: 8.0.100 Commit: 57efcf1350 Workload version: 8.0.100-manifests.a7f084b6

Runtime Environment: OS Name: Mac OS X OS Version: 14.5 OS Platform: Darwin RID: osx-arm64 Base Path: /usr/local/share/dotnet/sdk/8.0.100/

.NET workloads installed: Workload version: 8.0.100-manifests.a7f084b6 [wasm-tools] Installation Source: SDK 8.0.100 Manifest Version: 8.0.3/8.0.100 Manifest Path: /usr/local/share/dotnet/sdk-manifests/8.0.100/microsoft.net.workload.mono.toolchain.current/8.0.3/WorkloadManifest.json Install Type: FileBased

Host: Version: 8.0.4 Architecture: arm64 Commit: 2d7eea2529

.NET SDKs installed: 6.0.201 [/usr/local/share/dotnet/sdk] 6.0.402 [/usr/local/share/dotnet/sdk] 6.0.404 [/usr/local/share/dotnet/sdk] 7.0.100 [/usr/local/share/dotnet/sdk] 7.0.101 [/usr/local/share/dotnet/sdk] 7.0.302 [/usr/local/share/dotnet/sdk] 8.0.100-rc.1.23463.5 [/usr/local/share/dotnet/sdk] 8.0.100-rc.2.23502.2 [/usr/local/share/dotnet/sdk] 8.0.100 [/usr/local/share/dotnet/sdk]

.NET runtimes installed: Microsoft.AspNetCore.App 6.0.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.10 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.12 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.1 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.5 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.0-rc.1.23421.29 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.0-rc.2.23480.2 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 6.0.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.10 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.12 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.1 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.0-rc.1.23419.4 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.0-rc.2.23479.6 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.4 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

Other architectures found: x64 [/usr/local/share/dotnet/x64]

Environment variables: Not set

global.json file: Not found

Learn more: https://aka.ms/dotnet/info

Download .NET: https://aka.ms/dotnet/download

brad-technologik commented 2 months ago

Upon further investigation, it seems like this is not specific OpaqueRedirection, as this occurs even with streaming disabled. In the reproduction steps, change Weather.razor to disable streaming (@attribute [StreamRendering(false)] and try again. When inspecting dev tools, you'll still see the Location header of the page use http instead of https.

brad-technologik commented 2 months ago

I've created a simple middleware workaround that changes the redirect (Location header) to https if the original request was https scheme:

// Workaround for https://github.com/dotnet/aspnetcore/issues/57650.
app.Use(async (context, next) =>
{
    await next(context);
    if (context.Response.StatusCode == (int)HttpStatusCode.Redirect &&
        context.Request.IsHttps && context.Response.Headers.Location.Count > 0)
    {
        var locations = context.Response.Headers.Location.ToArray();
        for(int i=0; i<locations.Length; i++)
        {
            if (locations[i]!.StartsWith("http://"))
            {
                locations[i] = locations[i]!.Replace("http://", "https://");
            }
        }
        context.Response.Headers["Location"] = new StringValues(locations);

    }
});
javiercn commented 2 months ago

@brad-technologik thanks for contacting us.

This seems to be an issue on your specific setup. Is the scheme on the incoming request that triggers the redirect HTTPS?

It is very unlikely that this is caused by opaque redirect as opposed to a misconfiguration on your app side. Opaque Redirect is a very thin wrapper over response.Redirect and that simply uses whatever is available on HttpContext.

https://github.com/dotnet/aspnetcore/blob/6f71b2e2d561e8fa12a450b080e6713f8d9a4200/src/Components/Endpoints/src/Builder/OpaqueRedirection.cs#L92

I would suggest you log the scheme that you add custom middleware on your environment to detect and log the scheme of the incoming request that triggers the opaque redirection. It's very likely that it is not set as HTTPS and that's the source of the issue.

brad-technologik commented 2 months ago

@javiercn I think the bug lies in UseForwardedHeaders middleware.

In my repro, when I add logging, X-Forwarded-Proto is set to https, but context.Request.IsHttps is still false in downstream middleware:

api-1           | Referer:https://localhost/weather?
api-1           | Upgrade-Insecure-Requests:1
api-1           | X-Real-IP:192.168.65.1
api-1           | X-Forwarded-For:192.168.65.1
api-1           | X-Forwarded-Proto:https
api-1           | X-Forwarded-Host:my-site
api-1           | sec-ch-ua:"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"
api-1           | sec-ch-ua-mobile:?0
api-1           | sec-ch-ua-platform:"macOS"
api-1           | Sec-Fetch-Site:same-origin
api-1           | Sec-Fetch-Mode:navigate
api-1           | Sec-Fetch-User:?1
api-1           | Sec-Fetch-Dest:document
api-1           | context.Request.IsHttps: False

When I add the following middleware, the redirects work as expected:

app.Use((context, next) =>
    {
        if (context.Request.Headers["X-Forwarded-Proto"]=="https")
            context.Request.Scheme = "https";
        return next(context);
    });

However, I assumed that UseForwardedHeaders was supposed to do what the above does. Did I use UseForwardedHeaders incorrectly?

brad-technologik commented 2 months ago

Changing

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

To

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
});

fixed the issue, but I'm unclear why that worked, especially since that former is the example given in the documentation.

javiercn commented 2 months ago

@brad-technologik thanks for the additional details.

I'm not sure what can be going on. The logic for it is https://github.com/dotnet/aspnetcore/blob/2152f3ba4b7b7618b9b6f1065eb65711311bdf42/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs#L120

I'm not sure if it's a bug on the code or a bug in the docs.

halter73 commented 2 months ago

Can you include the trace-level ASP.NET Core logs from the ForwardedHeadersMiddleware?

You might be running into an issue because the middleware verifies that the client claiming to be the proxy has the expected remote IP address as noted in the doc's troubleshooting section.

The request's original remote IP must match an entry in the KnownProxies or KnownNetworks lists before forwarded headers are processed. This limits header spoofing by not accepting forwarders from untrusted proxies. When an unknown proxy is detected, logging indicates the address of the proxy:

dbug: Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware[1]
     Unknown proxy: [::ffff:10.0.0.100]:54321

By default, KnownProxies and KnownNetworks only contain [::1] and 127.0.0.0/8 respectively. I'm a little surprised that this only gets checked when ForwardedHeaders.XForwardedFor is selected and not ForwardedHeaders.XForwardedProto, but being able to spoof the scheme does seem like less of a security risk than being able to spoof a remote IP address.

If you know the IP address or subnet of the reverse proxy, I recommend adding it to KnownProxies or KnownNetworks. Otherwise, you can clear both lists to skip the check, but you need to beware that any client that can open a connection to your backend server will be able to spoof their IP address.

Another option is to set the ASPNETCORE_FORWARDEDHEADERS_ENABLED environment variable to true or otherwise set the "ForwardedHeaders_Enabled" IConfiguration to true.

To forward the scheme from the proxy in non-IIS scenarios, enable the Forwarded Headers Middleware by setting ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. Warning: This flag uses settings designed for cloud environments and doesn't enable features such as the KnownProxies option to restrict which IPs forwarders are accepted from.

https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-8.0#forward-the-scheme-for-linux-and-non-iis-reverse-proxies

If you do this, you will no longer need to call Configure<ForwardedHeadersOptions>(... or app.UseForwardedHeaders(); yourself. WebApplicationBuilder will set that up for you when it sees the "ForwardedHeaders_Enabled" config.

dotnet-policy-service[bot] commented 2 months ago

Hi @brad-technologik. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

dotnet-policy-service[bot] commented 1 month ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

See our Issue Management Policies for more information.