Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blazor forms should render target URL in action attribute as root-relative (absolute path), not fully qualified #51380

Closed
DamianEdwards opened this issue Oct 14, 2023 · 6 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components bug This issue describes a behavior which is not expected - a bug. feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future
Milestone

Comments

@DamianEdwards
Copy link
Member

DamianEdwards commented Oct 14, 2023

The change recently made to always render the current fully qualified request URL as the value for any form element rendered in a Razor component, causes issues when the app is deployed behind any kind of reverse proxy/ingress controller, which is extremely common for any hosting PaaS, including Azure App Service for Linux/Containers, and Azure Container Apps.

In these environments, the value of HttpRequest.Scheme, HttpRequest.Host, etc. will not reflect the values the browser client sees unless the forwarded headers middleware is added and properly configured. Doing so is a fairly complex task and tied strongly to the specific destination the app is deployed to.

Instead of using the fully qualified URL, MVC and Razor Pages renders the action attribute with just the root-relative (absolute) path, i.e. /shopping/cart instead of http://contoso.com/shopping/cart, so that the scheme and host from the client's point of view, do not need to be known (it does this using IUrlHelper). Note that this must still take into consideration the current value of HttpRequest.PathBase if set. This makes it far simpler to have the application work correctly without need to configured forwarded headers.

We should update Blazor's form rendering logic to match what MVC and Razor Pages does, except that Blazor should always render a value (MVC skips it if it's for the current controller/action/page).

@javiercn @SteveSandersonMS

@DamianEdwards DamianEdwards added the area-blazor Includes: Blazor, Razor Components label Oct 14, 2023
@mkArtakMSFT mkArtakMSFT added the bug This issue describes a behavior which is not expected - a bug. label Oct 14, 2023
DamianEdwards added a commit to dotnet/aspire that referenced this issue Oct 14, 2023
@halter73
Copy link
Member

halter73 commented Oct 15, 2023

It seems the workaround is:

-<EditForm action="@Navigation.Uri" ...>
+<EditForm action="@Navigation.ToBaseRelativePath(Navigation.Uri)" ...>

I'm not sure how you can expect anything other than an absolute URL for action="@Navigation.Uri" given the docs for NavigationManager.Uri say:

The Uri is always represented as an absolute URI in string form.

action="@Navigation.ToBaseRelativePath(Navigation.Uri)" seems more correct if you have to set an action. I get it's a lot uglier considering it's the thing you want more often. I think we should expose Navigation.ToBaseRelativePath(Navigation.Uri) directly as a property. We could call it NavigationManager.BaseRelativePath.

But why would one specify the action at all? We didn't do that for the Identity razor components, but those are always rendered statically.

@halter73
Copy link
Member

halter73 commented Oct 15, 2023

I did forget that I did use action for the logout form because that is not always rendered statically given it's in NavMenu.razor.

<form action="Account/Logout" method="post">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
<button type="submit" class="nav-link">
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
</button>
</form>

In this case, the action is well known ahead of time to be the relative "Account/Logout", but the ReturnUrl is defined using Navigation.ToBaseRelativePath because of the issues you mention with reverse proxies changing the path base.

currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
StateHasChanged();

@DamianEdwards
Copy link
Member Author

But why would one specify the action at all? We didn't do that for the Identity razor components, but those are always rendered statically.

I've done it in my app just to workaround this issue 😄. If I let Blazor render the action with the full uri (which it does by default now) then you get issues once the app is deployed to a host where TLS termination is done ahead of the app, as the form renders with an http address even though the page is being served over https and the browser blocks it.

DamianEdwards added a commit to dotnet/aspire that referenced this issue Oct 15, 2023
@DamianEdwards
Copy link
Member Author

Just hit an issue with the workaround 😞

If you use a base relative URL when you're already on the site root (/), what gets rendered is action="". An empty action value results in the reappearance of the original issue (#51118). I'm going to attempt to modify the workaround to ensure a blank action is never rendered, but rather a / is used if the call to ToBaseRelativePath returns an empty string.

@DamianEdwards
Copy link
Member Author

OK this extension method seems to resolve the issue with the original workaround. Note this could likely be implemented more efficiently directly in NavigationManager with span-based string manipulation:

namespace Microsoft.AspNetCore.Components;

public static class NavigationManagerExtensions
{
    public static string ToAbsolutePath(this NavigationManager navigationManager, string uri)
    {
        if (uri.StartsWith(navigationManager.BaseUri, StringComparison.Ordinal))
        {
            // The absolute URI must be of the form "{baseUri}something" (where baseUri ends with a slash),
            // and from that we return "something". If baseUri includes a path, we return that path too.
            return new Uri(uri).AbsolutePath;
        }

        var message = $"The URI '{uri}' is not contained by the base URI '{navigationManager.BaseUri}'.";
        throw new ArgumentException(message);
    }
}

@DamianEdwards DamianEdwards changed the title Blazor forms should render target URL in action attribute as root-relative, not absolute Blazor forms should render target URL in action attribute as root-relative (absolute path), not fully qualified Oct 15, 2023
@SteveSandersonMS
Copy link
Member

This is unfortunate - we specifically discussed this exact issue in the PR for #51130, so it seems there was a misunderstanding. I definitely agree this should work by default.

Returning a root-relative URL will make this work naturally in a much wider range of cases. PR: #51403

Caveat

Returning a root-relative URL is still not sufficient if a reverse proxy modifies the path in any way, e.g., mapping https://example.com/abc to http://webapp/v1/abc (it would return action="/v1/abc" but should be action="/abc").

A completely general fix is impossible because URL rewriting could be an arbitrary function and needn't even be invertible.

Even for the basic case where an ingress adds a path prefix, we can't invert it by default because we don't know that has happened. Given a request to http://webapp/v1/abc with PathBase=v1, we still assume that /v1 is part of the client-visible URL because there's nothing to tell us it isn't. So, we initialize NavigationManager.BaseUri=http://webapp/v1/ and NavigationManager.BaseUri=http://webapp/v1/abc. We can't generally assume that PathBase is not part of the client-visible URL, because in most cases it actually is. It doesn't even look like there's an HTTP header to tell us the full, pre-rewriting URL.

But despite this, MVC and Razor Pages have been getting along fine for years with this limitation, so presumably in practice it's not a problem.

Alternative client-side solution

The obvious alternative for Blazor is to handle this on the client, since the case that prompted all this is when we're doing enhanced nav. We could make it work with all possible URL transformations trivially in a single line of code, and would no longer have to force emitting an action attribute: https://github.com/dotnet/aspnetcore/pull/51404/files#diff-9e3ff8a35be230ce5d4600e611516b4e8826d87b1ecd0f9879e172e922bcbaa8R126

However on further consideration I don't think a client-side fix is what we want, because it would still run into trouble if a certain form is not enhanced but some other enhanced nav is still pending in the background. For example, the user clicks "back" which updates the URL instantly and begins an async enhanced page load, but before it completes they could submit a non-enhanced form without any action attribute. In that case it would then submit to the wrong place, which isn't acceptable.

Overall proposal

I agree with @DamianEdwards's proposal above: we emit a root-relative URL, despite the caveat. The fact that MVC and Razor Pages already do this is good evidence that it's sufficient in practice.

In the rare case where it's not sufficient, the developer will have to emit action manually (which presumably is the same as they would do in MVC/Razor Pages).

PR: #51403

@mkArtakMSFT mkArtakMSFT added this to the 8.0.0 milestone Oct 16, 2023
@mkArtakMSFT mkArtakMSFT added the feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future label Oct 16, 2023
mkArtakMSFT pushed a commit that referenced this issue Oct 16, 2023
# Fix form actions for apps deployed behind reverse proxy

Makes forms work for apps deployed behind a reverse proxy.

## Description

Apps deployed behind a reverse proxy (e.g., container apps) should not try to emit absolute URLs by default because the scheme/hostname/port may differ from what is reachable from the outside world. The fix is to emit root-relative URLs.

Fixes #51380

## Customer Impact

Without this fix, apps deployed behind a reverse proxy (e.g., in ACA) would not support form posts.

## Regression?

- [ ] Yes
- [x] No

No because this only affects SSR forms, which is a new feature in .NET 8.

## Risk

- [ ] High
- [ ] Medium
- [x] Low

Low because this is only a change to how we generate the URL for a form's `action` attribute. Previously we used an absolute URL, but now we use a root-relative one. There is no other runtime change. Everything else in this PR is extra tests and updating existing tests.

## Verification

- [x] Manual (required)
- [x] Automated

## Packaging changes reviewed?

- [ ] Yes
- [ ] No
- [x] N/A
@ghost ghost locked as resolved and limited conversation to collaborators Nov 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components bug This issue describes a behavior which is not expected - a bug. feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future
Projects
None yet
Development

No branches or pull requests

4 participants