Close outbound credential and metadata bypasses
This commit is contained in:
parent
3088aa439e
commit
c4402985af
1 changed files with 194 additions and 23 deletions
|
|
@ -32,6 +32,7 @@ struct ObservedRequest {
|
|||
host: Option<String>,
|
||||
authorization: Option<String>,
|
||||
cookie: Option<String>,
|
||||
api_key: Option<String>,
|
||||
}
|
||||
|
||||
impl ScriptedResolver {
|
||||
|
|
@ -134,6 +135,7 @@ async fn read_request(stream: &mut (impl AsyncRead + Unpin)) -> ObservedRequest
|
|||
host: headers.get("host").cloned(),
|
||||
authorization: headers.get("authorization").cloned(),
|
||||
cookie: headers.get("cookie").cloned(),
|
||||
api_key: headers.get("x-api-key").cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,9 +203,10 @@ async fn redirects_reauthorize_and_strip_cross_origin_credentials() {
|
|||
HeaderValue::from_static("Bearer synthetic"),
|
||||
);
|
||||
headers.insert("cookie", HeaderValue::from_static("session=synthetic"));
|
||||
headers.insert("x-api-key", HeaderValue::from_static("synthetic-api-key"));
|
||||
let request = OutboundRequest::get(&format!("http://source.test:{port}/redirect"))
|
||||
.expect("request")
|
||||
.with_headers(headers)
|
||||
.with_credential_headers(headers)
|
||||
.expect("headers");
|
||||
let result = client.execute(request).await.expect("response");
|
||||
assert_eq!(result.final_url().host_str(), "other.test");
|
||||
|
|
@ -214,12 +217,40 @@ async fn redirects_reauthorize_and_strip_cross_origin_credentials() {
|
|||
Some("Bearer synthetic")
|
||||
);
|
||||
assert_eq!(observed[0].cookie.as_deref(), Some("session=synthetic"));
|
||||
assert_eq!(observed[0].api_key.as_deref(), Some("synthetic-api-key"));
|
||||
assert_eq!(observed[1].authorization, None);
|
||||
assert_eq!(observed[1].cookie, None);
|
||||
assert_eq!(observed[1].api_key, None);
|
||||
assert_eq!(resolver.calls("source.test"), 1);
|
||||
assert_eq!(resolver.calls("other.test"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_headers_must_be_classified_as_credentials() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-api-key", HeaderValue::from_static("synthetic-api-key"));
|
||||
assert_eq!(
|
||||
OutboundRequest::get("https://example.com/")
|
||||
.expect("request")
|
||||
.with_public_headers(headers)
|
||||
.expect_err("unclassified header"),
|
||||
OutboundError::InvalidHeaders
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_debug_redacts_url_and_credential_values() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-api-key", HeaderValue::from_static("synthetic-api-key"));
|
||||
let request = OutboundRequest::get("https://example.com/sensitive?token=synthetic")
|
||||
.expect("request")
|
||||
.with_credential_headers(headers)
|
||||
.expect("credential headers");
|
||||
let debug = format!("{request:?}");
|
||||
assert!(!debug.contains("sensitive"));
|
||||
assert!(!debug.contains("synthetic-api-key"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn denied_redirect_is_rejected_before_dial() {
|
||||
let (port, _, server) = serve_http(1, |request| {
|
||||
|
|
@ -375,6 +406,62 @@ async fn tls_verification_uses_canonical_hostname_not_pinned_address() {
|
|||
server.await.expect("server");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ipv6_pinning_preserves_http_authority_and_tls_server_name() {
|
||||
let certified =
|
||||
rcgen::generate_simple_self_signed(vec!["source6.test".to_owned()]).expect("certificate");
|
||||
let certificate_der = certified.cert.der().clone();
|
||||
let key_der = certified.signing_key.serialize_der();
|
||||
let tls = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(
|
||||
vec![certificate_der.clone()],
|
||||
rustls::pki_types::PrivateKeyDer::Pkcs8(key_der.into()),
|
||||
)
|
||||
.expect("TLS config");
|
||||
let listener = match TcpListener::bind("[::1]:0").await {
|
||||
Ok(listener) => listener,
|
||||
Err(error)
|
||||
if error.kind() == io::ErrorKind::AddrNotAvailable
|
||||
&& std::env::var_os("REMORA_OUTBOUND_REQUIRE_IPV6").is_none() =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
Err(error) => panic!("IPv6 bind: {error}"),
|
||||
};
|
||||
let port = listener.local_addr().expect("address").port();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.expect("accept");
|
||||
let mut stream = tokio_rustls::TlsAcceptor::from(Arc::new(tls))
|
||||
.accept(stream)
|
||||
.await
|
||||
.expect("TLS handshake");
|
||||
let request = read_request(&mut stream).await;
|
||||
assert_eq!(
|
||||
request.host.as_deref(),
|
||||
Some(format!("source6.test:{port}").as_str())
|
||||
);
|
||||
stream
|
||||
.write_all(response("200 OK", &[], "ok").as_bytes())
|
||||
.await
|
||||
.expect("response");
|
||||
});
|
||||
let policy = OutboundPolicyBuilder::new()
|
||||
.allow_scheme_port("https", port)
|
||||
.allow_non_global("::1/128".parse::<IpNet>().expect("network"))
|
||||
.build();
|
||||
let resolver = ScriptedResolver::new([("source6.test", vec![vec![ip("::1")]])]);
|
||||
let root = reqwest::Certificate::from_der(certificate_der.as_ref()).expect("root");
|
||||
let client = OutboundClient::builder(resolver, policy)
|
||||
.add_root_certificate(root)
|
||||
.build();
|
||||
client
|
||||
.execute(OutboundRequest::get(&format!("https://source6.test:{port}/")).expect("request"))
|
||||
.await
|
||||
.expect("IPv6 TLS response");
|
||||
server.await.expect("server");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn https_redirect_cannot_downgrade_to_http() {
|
||||
let certified =
|
||||
|
|
|
|||
Loading…
Reference in a new issue