Files
sausage/host/tests/cred_security.rs
T

176 lines
5.4 KiB
Rust

//! 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<String, script::Error> {
Ok(self.0.to_string())
}
}
fn config(host: &str, port: Option<u16>) -> 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"
);
}