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

Use 308 status instead of 301 when redirecting #682

Merged
merged 13 commits into from
Jan 12, 2022

Conversation

nashley
Copy link
Contributor

@nashley nashley commented Jan 1, 2022

Motivation

Assuming a route for /path is defined,
POST /path/ currently may redirect to GET /path when it should redirect to POST /path (depending on the client).

This was outlined in #681.

This has potential security implications (e.g., if the POST contained sensitive data, that data may be saved by the user's client in their browser history and/or in the server's access logs).

It's worth noting that the internal test client, a wrapper for the popular reqwest client, is susceptible to this incorrect behavior.

Solution

This PR replaces the 301 redirect with a 308 and adds corresponding tests to verify the behavior (note that these tests fail without the accompanying code change).

  • Determine if this counts as a breaking change
    • if so, this should be rebased onto axum-next
    • if not (or regardless), an entry in CHANGELOG.md should be added

For redirects resulting from requests to paths with a trailing slash,
use 308 instead of 301 to prevent non-GET requests (POST, PUT, etc) from
being changed to GET.

For example, (assuming a route for /path is defined)...
  - Old behavior results in:
  POST /path/ -> GET /path

  - New behavior results in:
  POST /path/ -> POST /path

Fixes tokio-rs#681
@nashley
Copy link
Contributor Author

nashley commented Jan 1, 2022

Should this PR be against main or axum-next? The behavior is changing in a way that could be impactful [1][2], but I'd argue this fits more in line with a bugfix, so I went ahead and made it against main.

[1] e.g., if a server had a different route defined for the GET requests and the application somehow depended on the broken redirect functionality sending requests with a trailing / to the GET handler instead of the typically-desired handler
[2] Some clients (circa 2012) only support 301 and not 308 (e.g., Internet Explorer before version 11, Firefox before 14, Chrome before 36, etc). See caniuse for more info. Such clients would stop following trailing slash redirects properly.

@nashley nashley marked this pull request as draft January 1, 2022 03:38
@nashley
Copy link
Contributor Author

nashley commented Jan 1, 2022

Converted to draft while I add deprecation attributes to methods that return ambiguous response codes (viz., 301 and 302)

Nick Ashley added 3 commits January 1, 2022 04:49
Deprecates found() due to its use of HTTP 302
Use Redirect::permanent instead of re-implementing its functionality
@nashley
Copy link
Contributor Author

nashley commented Jan 1, 2022

It looks like there's currently no built-in method for 301 redirects -- just 302. I refrained from adding that in this PR (since it seems odd to add a new method that's immediately deprecated), but I can do so if desired.

Replace usages of Redirect:found with Redirect::to and Redirect::temporary as appropriate
@nashley
Copy link
Contributor Author

nashley commented Jan 1, 2022

I'm not yet marking this as ready for review, since I haven't tested examples/oauth/src/main.rs after making changes to it (though I expect it will work without a hitch).

@ttys3
Copy link
Contributor

ttys3 commented Jan 2, 2022

if we change 301 -> 308
I think leave 302 alone is strange. maybe we also need change 302 to 307

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307

The only difference between 307 and 302 is that 307 guarantees that the method and the body will not be changed when the redirected request is made. With 302, some old clients were incorrectly changing the method to GET: the behavior with non-GET methods and 302 is then unpredictable on the Web, whereas the behavior with 307 is predictable. For GET requests, their behavior is identical.

@nashley
Copy link
Contributor Author

nashley commented Jan 2, 2022

@ttys3 I agree. I had already marked Redirect::found as deprecated and updated the examples to reflect that. Did I miss any other instances of 302?
> rg StatusCode::FOUND -B 4 yields only the aforementioned hit:

axum/src/response/redirect.rs
87-    #[deprecated(
88-        note = "This results in different behavior between clients, so Redirect::temporary or Redirect::to should be used instead"
89-    )]
90-    pub fn found(uri: Uri) -> Self {
91:        Self::with_status_code(StatusCode::FOUND, uri)

Copy link
Member

@davidpdrsn davidpdrsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this!

IMO its not a breaking change so doesn't have to merged into axum-next.

Previously the example would panic if a request was made without the
`Cookie` header. Now the user is redirected to the login page as
expected.
@nashley
Copy link
Contributor Author

nashley commented Jan 4, 2022

I verified that the example still works and made some small improvements along the way (though I had to expose some previously-private fields to nicely destructure an existing error when requests without a Cookie header were made).

I'd like confirmation that exposing TypedHeaderRejection's fields directly instead of via accessor methods isn't a bad idea for some reason I can't foresee. But otherwise I think this is ready.

@nashley nashley marked this pull request as ready for review January 4, 2022 04:48
Comment on lines 205 to 215
impl From<TypedHeaderRejection> for AuthRedirect {
fn from(error: TypedHeaderRejection) -> Self {
match error {
TypedHeaderRejection {
name: &header::COOKIE,
reason: TypedHeaderRejectionReason::Missing,
} => AuthRedirect,
_ => panic!("unexpected error getting Cookie header(s)"),
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not hugely fond of this, but I couldn't think of a better way to do it without introducing a wrapping type or using anyhow in a way I'm not familiar with. Suggestions welcome.

Copy link
Member

@davidpdrsn davidpdrsn Jan 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very familiar with this example but the fields should stay private. Users should not be able to construct this type directly or mutate the fields.

Copy link
Member

@davidpdrsn davidpdrsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields of TypedHeaderRejection should stay private.

@nashley
Copy link
Contributor Author

nashley commented Jan 12, 2022

Apologies for all of the extra commits. I'm not squashing to abide by the contributing guidelines.

Copy link
Member

@davidpdrsn davidpdrsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGMT! Thanks 😃

@davidpdrsn
Copy link
Member

Apologies for all of the extra commits. I'm not squashing to abide by the contributing guidelines.

Appreciated! Makes reviewing changes since my last review much easier.

@davidpdrsn davidpdrsn merged commit 007a0e8 into tokio-rs:main Jan 12, 2022
@jplatte
Copy link
Member

jplatte commented Jan 12, 2022

Appreciated! Makes reviewing changes since my last review much easier.

You know GitHub can also render force-push diffs though, right? (need to click the text "force-pushed" in the comments view)

@davidpdrsn
Copy link
Member

Maybe 🤷 I remember not being able to figure it out

@jplatte
Copy link
Member

jplatte commented Jan 12, 2022

It is definitely weird, but it works (just requires a few extra clicks because the notification will lead you to a "we couldn't find these commits" page). Example: ruma/ruma#796 (comment)

@davidpdrsn
Copy link
Member

omg must subtle link ever! No wonder I never noticed 🧐

image

image

@nashley nashley deleted the use-308-for-tsr branch January 12, 2022 14:58
davidpdrsn added a commit that referenced this pull request Jan 13, 2022
- **fixed:** Fix using incorrect path prefix when nesting `Router`s at `/` ([#691])
- **fixed:** Make `nest("", service)` work and mean the same as `nest("/", service)` ([#691])
- **fixed:** Replace response code `301` with `308` for trailing slash redirects. Also deprecates
  `Redirect::found` (`302`) in favor of `Redirect::temporary` (`307`) or `Redirect::to` (`303`).
  This is to prevent clients from changing non-`GET` requests to `GET` requests ([#682])

[#691]: #691
[#682]: #682
@davidpdrsn davidpdrsn mentioned this pull request Jan 13, 2022
davidpdrsn added a commit that referenced this pull request Jan 13, 2022
- **fixed:** Fix using incorrect path prefix when nesting `Router`s at `/` ([#691])
- **fixed:** Make `nest("", service)` work and mean the same as `nest("/", service)` ([#691])
- **fixed:** Replace response code `301` with `308` for trailing slash redirects. Also deprecates
  `Redirect::found` (`302`) in favor of `Redirect::temporary` (`307`) or `Redirect::to` (`303`).
  This is to prevent clients from changing non-`GET` requests to `GET` requests ([#682])

[#691]: #691
[#682]: #682
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants