This commit is contained in:
Mateusz Gruszczyński committed 2026-10-11 13:56:30 +02:00
1 parent 95c55da2ee
commit d942c1a35d
42 files changed
+1265 -109

No files matched your search

+142
View File
@@ -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;
}