v0.31.4
This commit is contained in:
1 parent
95c55da2ee
commit
d942c1a35d
42 files changed
+1265
-109
No files matched your search
@@ -0,0 +1,142 @@
|
||||
#[test]
|
||||
fn pinned_admin_proxy_proto_is_bounded_and_requires_external_https() {
|
||||
let mut headers = HeaderMap::new();
|
||||
assert!(pinned_admin_proto_compatible(&headers));
|
||||
headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
|
||||
assert!(pinned_admin_proto_compatible(&headers));
|
||||
headers.insert("x-forwarded-proto", HeaderValue::from_static("https, http"));
|
||||
assert!(pinned_admin_proto_compatible(&headers));
|
||||
headers.append("x-forwarded-proto", HeaderValue::from_static("http"));
|
||||
assert!(pinned_admin_proto_compatible(&headers));
|
||||
for bad in ["http", "http, https", "https, ftp", "https,", ",https", "HTTPS", "https, http, http, http, http, http, http, http, http"] {
|
||||
headers.remove("x-forwarded-proto");
|
||||
headers.insert("x-forwarded-proto", HeaderValue::from_str(bad).unwrap());
|
||||
assert!(!pinned_admin_proto_compatible(&headers), "must reject {bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pinned_panel_origin_survives_multi_hop_proxy_without_relaxing_csrf() {
|
||||
let mut config = Config::default();
|
||||
config.administration.public_origin = Some("https://panel.example.test".into());
|
||||
let (router, store, _dir, secret, csrf) = fixture_with_config(config).await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
// Without an explicit origin pin, 0.31.3 rejected this session GET as
|
||||
// external_scheme although the browser address remained correct.
|
||||
let result = router.clone().oneshot(request("/api/auth/session")
|
||||
.header("cookie", &cookie)
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.header("sec-fetch-site", "same-origin")
|
||||
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(result.status(), StatusCode::OK);
|
||||
let session_cookie = result.headers().get_all(SET_COOKIE).iter()
|
||||
.filter_map(|v| v.to_str().ok()).find(|v| v.starts_with(auth::COOKIE_NAME)).unwrap();
|
||||
assert!(session_cookie.contains("; Secure"));
|
||||
|
||||
// The incoming admin Host may itself be the pinned public DNS name.
|
||||
// There is no separate allowed_hosts entry in the saved configuration.
|
||||
let pinned_host = Request::builder().uri("/api/auth/session")
|
||||
.header("host", "panel.example.test")
|
||||
.header("cookie", &cookie)
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.extension(PeerAddr("127.0.0.1:45003".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap();
|
||||
assert_eq!(router.clone().oneshot(pinned_host).await.unwrap().status(), StatusCode::OK);
|
||||
|
||||
// Explicit Origin, CSRF token and app request marker remain mandatory
|
||||
// for a state-changing request even when XFP contains multiple hops.
|
||||
let unauthorized = router.clone().oneshot(request("/api/auth/logout")
|
||||
.method("POST").header("cookie", &cookie)
|
||||
.header("origin", "https://panel.example.test")
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.extension(PeerAddr("127.0.0.1:45001".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(unauthorized.status(), StatusCode::FORBIDDEN);
|
||||
let authorized = router.oneshot(request("/api/auth/logout")
|
||||
.method("POST").header("cookie", &cookie)
|
||||
.header("origin", "https://panel.example.test")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", csrf)
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.extension(PeerAddr("127.0.0.1:45002".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(authorized.status(), StatusCode::OK);
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pinned_panel_origin_rejects_untrusted_proxy_host_and_origin() {
|
||||
let mut config = Config::default();
|
||||
config.administration.public_origin = Some("https://panel.example.test".into());
|
||||
let (router, store, _dir, secret, _) = fixture_with_config(config).await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
for (xfh, xfp, origin, fetch_site) in [
|
||||
("evil.example.test", "https, http", "https://panel.example.test", "same-origin"),
|
||||
("other@panel.example.test", "https, http", "https://panel.example.test", "same-origin"),
|
||||
("panel.example.test", "http", "https://panel.example.test", "same-origin"),
|
||||
("panel.example.test", "http, https", "https://panel.example.test", "same-origin"),
|
||||
("panel.example.test", "https, http", "https://evil.example.test", "same-origin"),
|
||||
("panel.example.test", "https, http", "null", "same-origin"),
|
||||
("panel.example.test", "https, http", "https://panel.example.test", "cross-site"),
|
||||
] {
|
||||
let result = router.clone().oneshot(request("/api/auth/session")
|
||||
.header("cookie", &cookie)
|
||||
.header("x-forwarded-host", xfh)
|
||||
.header("x-forwarded-proto", xfp)
|
||||
.header("origin", origin)
|
||||
.header("sec-fetch-site", fetch_site)
|
||||
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(result.status(), StatusCode::FORBIDDEN,
|
||||
"must reject forwarded-host={xfh}, proto={xfp}, origin={origin}, fetch={fetch_site}");
|
||||
}
|
||||
// Credential-bearing certificate automation does not inherit the
|
||||
// browser-origin override, even through the same trusted admin socket.
|
||||
let automation = router.clone().oneshot(request("/api/certificates/deploy")
|
||||
.method("POST")
|
||||
.header(CERTIFICATE_API_KEY_HEADER, "svc-key")
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(automation.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
let untrusted = router.clone().oneshot(request("/api/auth/session")
|
||||
.header("cookie", &cookie)
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.header("origin", "https://panel.example.test")
|
||||
.extension(PeerAddr("192.0.2.42:44000".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(untrusted.status(), StatusCode::FORBIDDEN);
|
||||
|
||||
let mut duplicated = request("/api/auth/session")
|
||||
.header("cookie", &cookie)
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap();
|
||||
duplicated.headers_mut().append("x-forwarded-host", "panel.example.test".parse().unwrap());
|
||||
assert_eq!(router.clone().oneshot(duplicated).await.unwrap().status(), StatusCode::FORBIDDEN);
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn without_public_origin_ambiguous_forwarded_proto_stays_forbidden() {
|
||||
let mut config = Config::default();
|
||||
config.administration.allowed_hosts = vec!["panel.example.test".into()];
|
||||
let (router, store, _dir, secret, _) = fixture_with_config(config).await;
|
||||
let result = router.oneshot(request("/api/auth/session")
|
||||
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME))
|
||||
.header("x-forwarded-host", "panel.example.test")
|
||||
.header("x-forwarded-proto", "https, http")
|
||||
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||
.body(Body::empty()).unwrap()).await.unwrap();
|
||||
assert_eq!(result.status(), StatusCode::FORBIDDEN);
|
||||
store.close().await;
|
||||
}
|
||||
Reference in new issue
Block a user