1use prick_core::keyname;
17
18#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
20#[non_exhaustive]
21pub enum GuardError {
22 #[error(
24 "refusing to set `{name}` in the child environment: it is interpreted before the \
25 program starts, so its value controls what code runs. Pass --allow-unsafe-env to \
26 override."
27 )]
28 LoaderControlled {
29 name: String,
31 },
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub struct EnvGuard {
37 allow_unsafe: bool,
38}
39
40impl EnvGuard {
41 pub fn strict() -> Self {
43 Self { allow_unsafe: false }
44 }
45
46 pub fn permissive() -> Self {
48 Self { allow_unsafe: true }
49 }
50
51 pub fn check(self, name: &str) -> Result<(), GuardError> {
58 if !self.allow_unsafe && keyname::is_loader_controlled(name) {
59 return Err(GuardError::LoaderControlled { name: name.to_owned() });
60 }
61 Ok(())
62 }
63
64 pub fn check_all<'a>(self, names: impl IntoIterator<Item = &'a str>) -> Result<(), GuardError> {
74 for name in names {
75 self.check(name)?;
76 }
77 Ok(())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn the_default_policy_is_strict() {
87 assert_eq!(EnvGuard::default(), EnvGuard::strict());
88 }
89
90 #[test]
91 fn loader_controlled_names_are_refused_by_default() {
92 let guard = EnvGuard::strict();
93 for name in ["LD_PRELOAD", "DYLD_INSERT_LIBRARIES", "PATH", "NODE_OPTIONS", "BASH_ENV"] {
94 assert_eq!(
95 guard.check(name),
96 Err(GuardError::LoaderControlled { name: name.to_owned() }),
97 "{name} was not refused"
98 );
99 }
100 }
101
102 #[test]
103 fn ordinary_names_pass() {
104 let guard = EnvGuard::strict();
105 for name in ["DATABASE_URL", "API_KEY", "STRIPE_SECRET"] {
106 assert_eq!(guard.check(name), Ok(()), "{name} was wrongly refused");
107 }
108 }
109
110 #[test]
111 fn the_opt_in_allows_everything() {
112 let guard = EnvGuard::permissive();
113 assert_eq!(guard.check("LD_PRELOAD"), Ok(()));
114 assert_eq!(guard.check("DATABASE_URL"), Ok(()));
115 }
116
117 #[test]
118 fn a_single_refusal_fails_the_whole_set() {
119 let guard = EnvGuard::strict();
120 let names = ["SAFE_ONE", "LD_PRELOAD", "SAFE_TWO"];
121 assert_eq!(
122 guard.check_all(names),
123 Err(GuardError::LoaderControlled { name: "LD_PRELOAD".to_owned() })
124 );
125 assert_eq!(guard.check_all(["SAFE_ONE", "SAFE_TWO"]), Ok(()));
126 }
127
128 #[test]
129 fn the_refusal_message_names_the_override() {
130 let err = EnvGuard::strict().check("LD_PRELOAD").unwrap_err();
131 let message = err.to_string();
132 assert!(message.contains("LD_PRELOAD"));
133 assert!(message.contains("--allow-unsafe-env"));
134 }
135}