cedar_policy_validator/
str_checks.rs1use cedar_policy_core::ast::{Pattern, PolicyID, Template};
18
19use crate::expr_iterator::expr_text;
20use crate::expr_iterator::TextKind;
21use unicode_security::GeneralSecurityProfile;
22use unicode_security::MixedScript;
23
24#[derive(Debug, Clone)]
25pub struct ValidationWarning<'a> {
26 location: &'a PolicyID,
27 kind: ValidationWarningKind,
28}
29
30impl<'a> ValidationWarning<'a> {
31 pub fn location(&self) -> &'a PolicyID {
32 self.location
33 }
34
35 pub fn kind(&self) -> &ValidationWarningKind {
36 &self.kind
37 }
38
39 pub fn to_kind_and_location(self) -> (&'a PolicyID, ValidationWarningKind) {
40 (self.location, self.kind)
41 }
42}
43
44impl std::fmt::Display for ValidationWarning<'_> {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(
47 f,
48 "Warning: {} in policy with ID {}",
49 self.kind, self.location
50 )
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
55pub enum ValidationWarningKind {
56 MixedScriptString(String),
57 BidiCharsInString(String),
58 BidiCharsInIdentifier(String),
59 MixedScriptIdentifier(String),
60 ConfusableIdentifier(String),
61}
62
63impl std::fmt::Display for ValidationWarningKind {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 ValidationWarningKind::MixedScriptString(s) =>
67 write!(f, "The string \"{s}\" contains mixed scripts. This can cause characters to be confused for each other."),
68 ValidationWarningKind::MixedScriptIdentifier(s) => write!(f, "The identifier \"{s}\" contains mixed scripts. This can cause characters to be confused for each other."),
69 ValidationWarningKind::BidiCharsInString(s) => write!(f, "The string \"{s}\" contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow."),
70 ValidationWarningKind::BidiCharsInIdentifier(s) => write!(f, "The identifier \"{s}\" contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow."),
71 ValidationWarningKind::ConfusableIdentifier(s) => write!(f, "The identifier \"{s}\" contains characters that fall outside of the General Security Profile for Identifiers. We recommend adhering to this if possible. See Unicode® Technical Standard #39 for more info."),
72 }
73 }
74}
75
76pub fn confusable_string_checks<'a>(
78 p: impl Iterator<Item = &'a Template>,
79) -> impl Iterator<Item = ValidationWarning<'a>> {
80 let mut warnings = vec![];
81
82 for policy in p {
83 let e = policy.condition();
84 for str in expr_text(&e) {
85 let warning = match str {
86 TextKind::String(s) => permissable_str(s),
87 TextKind::Identifier(i) => permissable_ident(i),
88 TextKind::Pattern(p) => {
89 let pat = Pattern::new(p.iter().copied());
90 let as_str = format!("{pat}");
91 permissable_str(&as_str)
92 }
93 };
94
95 if let Some(w) = warning {
96 warnings.push(ValidationWarning {
97 location: policy.id(),
98 kind: w,
99 })
100 }
101 }
102 }
103
104 warnings.into_iter()
105}
106
107fn permissable_str(s: &str) -> Option<ValidationWarningKind> {
108 if s.chars().any(is_bidi_char) {
109 Some(ValidationWarningKind::BidiCharsInString(s.to_string()))
110 } else if !s.is_single_script() {
111 Some(ValidationWarningKind::MixedScriptString(s.to_string()))
112 } else {
113 None
114 }
115}
116
117fn permissable_ident(s: &str) -> Option<ValidationWarningKind> {
118 if s.chars().any(is_bidi_char) {
119 Some(ValidationWarningKind::BidiCharsInIdentifier(s.to_string()))
120 } else if !s.chars().all(|c| c.identifier_allowed()) {
121 Some(ValidationWarningKind::ConfusableIdentifier(s.to_string()))
122 } else if !s.is_single_script() {
123 Some(ValidationWarningKind::MixedScriptIdentifier(s.to_string()))
124 } else {
125 None
126 }
127}
128
129fn is_bidi_char(c: char) -> bool {
130 BIDI_CHARS.iter().any(|bidi| bidi == &c)
131}
132
133const BIDI_CHARS: [char; 9] = [
138 '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}',
139 '\u{2069}',
140];
141
142#[cfg(test)]
143mod test {
144
145 use super::*;
146 use cedar_policy_core::{ast::PolicySet, parser::parse_policy};
147
148 #[test]
149 fn strs() {
150 assert!(permissable_str("test").is_none());
151 assert!(permissable_str("test\t\t").is_none());
152 match permissable_str("say_һello") {
153 Some(ValidationWarningKind::MixedScriptString(_)) => (),
154 o => panic!("should have produced MixedScriptString: {:?}", o),
155 };
156 }
157
158 #[test]
159 #[allow(clippy::invisible_characters)]
160 fn idents() {
161 assert!(permissable_ident("test").is_none());
162 match permissable_ident("isAdmin") {
163 Some(ValidationWarningKind::ConfusableIdentifier(_)) => (),
164 o => panic!("should have produced ConfusableIdentifier: {:?}", o),
165 };
166 match permissable_ident("say_һello") {
167 Some(ValidationWarningKind::MixedScriptIdentifier(_)) => (),
168 o => panic!("should have produced MixedScriptIdentifier: {:?}", o),
169 };
170 }
171
172 #[test]
173 fn a() {
174 let src = r#"
175 permit(principal == test::"say_һello", action, resource);
176 "#;
177
178 let mut s = PolicySet::new();
179 let p = parse_policy(Some("test".to_string()), src).unwrap();
180 s.add_static(p).unwrap();
181 let warnings =
182 confusable_string_checks(s.policies().map(|p| p.template())).collect::<Vec<_>>();
183 assert_eq!(warnings.len(), 1);
184 let warning = &warnings[0];
185 let kind = warning.kind().clone();
186 let location = warning.location();
187 assert_eq!(
188 kind,
189 ValidationWarningKind::MixedScriptIdentifier(r#"say_һello"#.to_string())
190 );
191 assert_eq!(format!("{warning}"), "Warning: The identifier \"say_һello\" contains mixed scripts. This can cause characters to be confused for each other. in policy with ID test");
192 assert_eq!(location, &PolicyID::from_string("test"));
193 assert_eq!(warning.clone().to_kind_and_location(), (location, kind));
194 }
195
196 #[test]
197 #[allow(clippy::invisible_characters)]
198 fn b() {
199 let src = r#"
200 permit(principal, action, resource) when {
201 principal["isAdmin"] == "say_һello"
202 };
203 "#;
204 let mut s = PolicySet::new();
205 let p = parse_policy(Some("test".to_string()), src).unwrap();
206 s.add_static(p).unwrap();
207 let warnings = confusable_string_checks(s.policies().map(|p| p.template()));
208 assert_eq!(warnings.count(), 2);
209 }
210
211 #[test]
212 fn problem_in_pattern() {
213 let src = r#"
214 permit(principal, action, resource) when {
215 principal.name like "*_һello"
216 };
217 "#;
218 let mut s = PolicySet::new();
219 let p = parse_policy(Some("test".to_string()), src).unwrap();
220 s.add_static(p).unwrap();
221 let warnings =
222 confusable_string_checks(s.policies().map(|p| p.template())).collect::<Vec<_>>();
223 assert_eq!(warnings.len(), 1);
224 let warning = &warnings[0];
225 let kind = warning.kind().clone();
226 let location = warning.location();
227 assert_eq!(
228 kind,
229 ValidationWarningKind::MixedScriptString(r#"*_һello"#.to_string())
230 );
231 assert_eq!(format!("{warning}"), "Warning: The string \"*_һello\" contains mixed scripts. This can cause characters to be confused for each other. in policy with ID test");
232 assert_eq!(location, &PolicyID::from_string("test"));
233 assert_eq!(warning.clone().to_kind_and_location(), (location, kind));
234 }
235
236 #[test]
237 #[allow(text_direction_codepoint_in_literal)]
238 fn trojan_source() {
239 let src = r#"
240 permit(principal, action, resource) when {
241 principal.access_level != "user && principal.is_admin "
242 };
243 "#;
244 let mut s = PolicySet::new();
245 let p = parse_policy(Some("test".to_string()), src).unwrap();
246 s.add_static(p).unwrap();
247 let warnings =
248 confusable_string_checks(s.policies().map(|p| p.template())).collect::<Vec<_>>();
249 assert_eq!(warnings.len(), 1);
250 let warning = &warnings[0];
251 let kind = warning.kind().clone();
252 let location = warning.location();
253 assert_eq!(
254 kind,
255 ValidationWarningKind::BidiCharsInString(r#"user && principal.is_admin "#.to_string())
256 );
257 assert_eq!(format!("{warning}"), "Warning: The string \"user && principal.is_admin \" contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow. in policy with ID test");
258 assert_eq!(location, &PolicyID::from_string("test"));
259 assert_eq!(warning.clone().to_kind_and_location(), (location, kind));
260 }
261}