176 lines
5.4 KiB
Rust
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"
|
|
);
|
|
}
|