//! Destination matching fails closed before and after a credential transform. use sausage::cred::{ inject::Outcome, items::Item, script::{self, Limits, Script}, *, }; use serde_json::json; struct Transform(serde_json::Value); impl Script for Transform { fn evaluate( &self, _: &str, _: &str, _: Option<&str>, _: Limits, ) -> std::result::Result { Ok(self.0.to_string()) } } fn config(host: &str, port: Option) -> Item { serde_json::from_value(json!({"name":"destination","free_use":true,"rules":[{"scheme":"https","hosts":[host],"port":port,"path_prefixes":["/allowed/"],"methods":null}],"validate_js":null,"transform_js":"transform","fields":[]})).unwrap() } fn parts(uri: &str) -> http::request::Parts { http::Request::get(uri).body(()).unwrap().into_parts().0 } #[test] fn reserved_host_is_rejected_in_every_case() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller::default(); for host in [ "host.sausage", "HOST.SAUSAGE", "Host.Sausage", "hOsT.sAuSaGe", ] { assert!( c.create_item(config(host, None), &caller, c.unlocked().unwrap()) .is_err() ); } } #[test] fn ports_before_and_after_transform() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller::default(); c.create_item(config("api.example", None), &caller, c.unlocked().unwrap()) .unwrap(); let Outcome::Forward(p) = c .inject( parts("https://api.example:9999/allowed/"), &caller, &Transform(json!({"headers":[{"name":"x-injected","value":"synthetic"}]})), ) .unwrap() else { panic!("denied") }; assert!(!p.headers.contains_key("x-injected")); assert!( c.inject( parts("https://api.example/allowed/"), &caller, &Transform(json!({"url":"https://api.example:9999/allowed/"})) ) .is_err() ); } #[test] fn local_rules_require_ports_and_ipv6_matches() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller::default(); for host in ["localhost", "127.0.0.1", "::1", "169.254.1.2", "fe80::1"] { assert!( c.create_item(config(host, None), &caller, c.unlocked().unwrap()) .is_err() ); } c.create_item(config("::1", Some(8443)), &caller, c.unlocked().unwrap()) .unwrap(); let Outcome::Forward(p) = c .inject( parts("https://[::1]:8443/allowed/"), &caller, &Transform(json!({"headers":[{"name":"x-injected","value":"synthetic"}]})), ) .unwrap() else { panic!("denied") }; assert!(p.headers.contains_key("x-injected")); } #[test] fn unsafe_paths_fail_before_and_after_transform() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller::default(); c.create_item(config("api.example", None), &caller, c.unlocked().unwrap()) .unwrap(); for tail in [ "./x", "../x", "%2e/x", "%2E/x", "x%2fy", "x%2Fy", "x%5cy", "x%5Cy", "x\\y", ] { let url = format!("https://api.example/allowed/{tail}"); assert!( c.inject(parts(&url), &caller, &Transform(json!({}))) .is_err() ); assert!( c.inject( parts("https://api.example/allowed/"), &caller, &Transform(json!({"url":url})) ) .is_err() ); } } #[test] fn transformed_authority_and_sensitive_headers() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller::default(); c.create_item(config("api.example", None), &caller, c.unlocked().unwrap()) .unwrap(); let Outcome::Forward(p) = c.inject(parts("https://api.example/allowed/"),&caller,&Transform(json!({"headers":[{"name":"host","value":"wrong.example"},{"name":"authorization","value":"synthetic"}]}))).unwrap() else { panic!("denied") }; assert!(p.headers["authorization"].is_sensitive()); assert_eq!(p.headers["host"], "api.example"); assert!( c.inject( parts("https://api.example/allowed/"), &caller, &Transform(json!({"url":"https://user@api.example/allowed/"})) ) .is_err() ); } #[test] fn oauth2_items_fail_without_exposing_refresh_values() { let dir = tempfile::tempdir().unwrap(); let c = Cred::open(dir.path()).unwrap(); let caller = Caller::default(); let mut item = config("api.example", None); item.fields.push(items::Field { field_id: String::new(), name: "refresh".into(), public: false, value: None, needs_value: true, field_type: types::FieldType::Oauth2RefreshToken, key_type: None, }); let item = c.create_item(item, &caller, c.unlocked().unwrap()).unwrap(); c.set_field( &item.id, &item.fields[0].field_id, "synthetic", &caller, c.unlocked().unwrap(), ) .unwrap(); assert_eq!( c.inject( parts("https://api.example/allowed/"), &caller, &Transform(json!({})) ) .err() .unwrap() .0, "oauth2_item" ); }