mirror of https://github.com/fafhrd91/actix-web
fix(router,web): fix panic when normalizing and rewriting paths (#3919)
This commit is contained in:
parent
7d81d7b5c8
commit
0fb2527c60
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.88.
|
- Minimum supported Rust version (MSRV) is now 1.88.
|
||||||
- Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881]
|
- Support `deserialize_any` in `PathDeserializer` (enables derived `#[serde(untagged)]` enums in path segments). [#2881]
|
||||||
|
- Fix stale path segment indices after path rewrites, preventing out-of-bounds access during extraction. [#3562]
|
||||||
|
|
||||||
[#2881]: https://github.com/actix/actix-web/pull/2881
|
[#2881]: https://github.com/actix/actix-web/pull/2881
|
||||||
|
[#3562]: https://github.com/actix/actix-web/issues/3562
|
||||||
|
|
||||||
## 0.5.3
|
## 0.5.3
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,45 @@ impl<T: ResourcePath> Path<T> {
|
||||||
self.segments.clear();
|
self.segments.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set new path while preserving and remapping existing captured segment indices.
|
||||||
|
///
|
||||||
|
/// The `reindex` closure maps byte indices from the previous path to byte indices in the new
|
||||||
|
/// path.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn update_with_reindex<F>(&mut self, path: T, mut reindex: F)
|
||||||
|
where
|
||||||
|
F: FnMut(u16) -> u16,
|
||||||
|
{
|
||||||
|
self.skip = reindex(self.skip);
|
||||||
|
|
||||||
|
for (_, item) in &mut self.segments {
|
||||||
|
if let PathItem::Segment(start, end) = item {
|
||||||
|
*start = reindex(*start);
|
||||||
|
*end = reindex(*end);
|
||||||
|
|
||||||
|
if *start > *end {
|
||||||
|
*start = *end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.path = path;
|
||||||
|
let path = self.path.path();
|
||||||
|
|
||||||
|
self.skip = clamp_to_char_boundary(path, self.skip);
|
||||||
|
|
||||||
|
for (_, item) in &mut self.segments {
|
||||||
|
if let PathItem::Segment(start, end) = item {
|
||||||
|
*start = clamp_to_char_boundary(path, *start);
|
||||||
|
*end = clamp_to_char_boundary(path, *end);
|
||||||
|
|
||||||
|
if *start > *end {
|
||||||
|
*start = *end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reset state.
|
/// Reset state.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
|
|
@ -179,6 +218,16 @@ impl<T: ResourcePath> Path<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clamp_to_char_boundary(path: &str, idx: u16) -> u16 {
|
||||||
|
let mut idx = usize::from(idx).min(path.len());
|
||||||
|
|
||||||
|
while idx > 0 && !path.is_char_boundary(idx) {
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx as u16
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PathIter<'a, T> {
|
pub struct PathIter<'a, T> {
|
||||||
idx: usize,
|
idx: usize,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@
|
||||||
- Ignore unparsable cookies in `Cookie` request header.
|
- Ignore unparsable cookies in `Cookie` request header.
|
||||||
- Add `experimental-introspection` feature to report configured routes [#3594]
|
- Add `experimental-introspection` feature to report configured routes [#3594]
|
||||||
- Add config/method for `TCP_NODELAY`. [#3918]
|
- Add config/method for `TCP_NODELAY`. [#3918]
|
||||||
|
- Fix panic when `NormalizePath` rewrites a scoped dynamic path before extraction (e.g., `scope("{tail:.*}")` + `Path<String>`). [#3562]
|
||||||
|
|
||||||
[#3895]: https://github.com/actix/actix-web/pull/3895
|
[#3895]: https://github.com/actix/actix-web/pull/3895
|
||||||
[#3594]: https://github.com/actix/actix-web/pull/3594
|
[#3594]: https://github.com/actix/actix-web/pull/3594
|
||||||
[#3918]: https://github.com/actix/actix-web/pull/3918
|
[#3918]: https://github.com/actix/actix-web/pull/3918
|
||||||
|
[#3562]: https://github.com/actix/actix-web/issues/3562
|
||||||
|
|
||||||
## 4.12.1
|
## 4.12.1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! For middleware documentation, see [`NormalizePath`].
|
//! For middleware documentation, see [`NormalizePath`].
|
||||||
|
|
||||||
use actix_http::uri::{PathAndQuery, Uri};
|
use actix_http::uri::{PathAndQuery, Uri};
|
||||||
|
use actix_router::Url;
|
||||||
use actix_service::{Service, Transform};
|
use actix_service::{Service, Transform};
|
||||||
use actix_utils::future::{ready, Ready};
|
use actix_utils::future::{ready, Ready};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
@ -14,6 +15,28 @@ use crate::{
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn build_byte_index_map(old_path: &str, new_path: &str) -> Vec<u16> {
|
||||||
|
let old_path = old_path.as_bytes();
|
||||||
|
let new_path = new_path.as_bytes();
|
||||||
|
|
||||||
|
let mut map = Vec::with_capacity(old_path.len() + 1);
|
||||||
|
map.push(0);
|
||||||
|
|
||||||
|
let mut old_idx = 0usize;
|
||||||
|
let mut new_idx = 0usize;
|
||||||
|
|
||||||
|
while old_idx < old_path.len() {
|
||||||
|
if new_idx < new_path.len() && old_path[old_idx] == new_path[new_idx] {
|
||||||
|
new_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
old_idx += 1;
|
||||||
|
map.push(new_idx.min(u16::MAX as usize) as u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
/// Determines the behavior of the [`NormalizePath`] middleware.
|
/// Determines the behavior of the [`NormalizePath`] middleware.
|
||||||
///
|
///
|
||||||
/// The default is `TrailingSlash::Trim`.
|
/// The default is `TrailingSlash::Trim`.
|
||||||
|
|
@ -183,6 +206,7 @@ where
|
||||||
// Both of the paths have the same length,
|
// Both of the paths have the same length,
|
||||||
// so the change can not be deduced from the length comparison
|
// so the change can not be deduced from the length comparison
|
||||||
if path != original_path {
|
if path != original_path {
|
||||||
|
let reindex = build_byte_index_map(original_path, path);
|
||||||
let mut parts = head.uri.clone().into_parts();
|
let mut parts = head.uri.clone().into_parts();
|
||||||
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
|
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
|
||||||
|
|
||||||
|
|
@ -193,7 +217,11 @@ where
|
||||||
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
|
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
|
||||||
|
|
||||||
let uri = Uri::from_parts(parts).unwrap();
|
let uri = Uri::from_parts(parts).unwrap();
|
||||||
req.match_info_mut().get_mut().update(&uri);
|
req.match_info_mut()
|
||||||
|
.update_with_reindex(Url::new(uri.clone()), |idx| {
|
||||||
|
let idx = usize::from(idx).min(reindex.len() - 1);
|
||||||
|
reindex[idx]
|
||||||
|
});
|
||||||
req.head_mut().uri = uri;
|
req.head_mut().uri = uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +237,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
guard::fn_guard,
|
guard::fn_guard,
|
||||||
test::{call_service, init_service, TestRequest},
|
test::{call_service, init_service, read_body, TestRequest},
|
||||||
web, App, HttpResponse,
|
web, App, HttpResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -406,6 +434,45 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn scope_dynamic_tail_path_is_reindexed() {
|
||||||
|
async fn handler(path: web::Path<String>) -> HttpResponse {
|
||||||
|
HttpResponse::Ok().body(path.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = init_service(
|
||||||
|
App::new().service(
|
||||||
|
web::scope("{tail:.*}")
|
||||||
|
.wrap(NormalizePath::trim())
|
||||||
|
.default_service(web::to(handler)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/uaie//iuaei").to_request();
|
||||||
|
let res = call_service(&app, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert_eq!(read_body(res).await, Bytes::from_static(b"uaie/iuaei"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn scope_static_prefix_skip_is_reindexed() {
|
||||||
|
let app = init_service(
|
||||||
|
App::new().service(
|
||||||
|
web::scope("/api")
|
||||||
|
.wrap(NormalizePath::trim())
|
||||||
|
.service(web::resource("/v1").to(HttpResponse::Ok)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let req = TestRequest::with_uri("/api//v1").to_request();
|
||||||
|
let res = call_service(&app, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn no_path() {
|
async fn no_path() {
|
||||||
let app = init_service(
|
let app = init_service(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue