1use relay_event_schema::protocol::Csp;
6
7use crate::origin::{SchemeDomainPort, matches_any_origin};
8use crate::{CspFilterConfig, FilterStatKey, Filterable};
9
10fn matches<It, S>(csp: Option<&Csp>, disallowed_sources: It) -> bool
12where
13 It: IntoIterator<Item = S>,
14 S: AsRef<str>,
15{
16 let disallowed_sources: Vec<SchemeDomainPort> = disallowed_sources
18 .into_iter()
19 .map(|origin| -> SchemeDomainPort { origin.as_ref().into() })
20 .collect();
21
22 if let Some(csp) = csp {
23 if matches_any_origin(csp.blocked_uri.as_str(), &disallowed_sources) {
24 return true;
25 }
26 if matches_any_origin(csp.source_file.as_str(), &disallowed_sources) {
27 return true;
28 }
29 if matches_any_origin(csp.document_uri.as_str(), &disallowed_sources) {
30 return true;
31 }
32 }
33 false
34}
35
36pub fn should_filter<F>(item: &F, config: &CspFilterConfig) -> Result<(), FilterStatKey>
38where
39 F: Filterable,
40{
41 if matches(item.csp(), &config.disallowed_sources) {
42 Err(FilterStatKey::InvalidCsp)
43 } else {
44 Ok(())
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use relay_event_schema::protocol::{Csp, Event, EventType};
51 use relay_protocol::Annotated;
52
53 use super::*;
54
55 fn get_csp_event(
56 blocked_uri: Option<&str>,
57 source_file: Option<&str>,
58 document_uri: Option<&str>,
59 ) -> Event {
60 fn annotated_string_or_none(val: Option<&str>) -> Annotated<String> {
61 match val {
62 None => Annotated::empty(),
63 Some(val) => Annotated::from(val.to_owned()),
64 }
65 }
66 Event {
67 ty: Annotated::from(EventType::Csp),
68 csp: Annotated::from(Csp {
69 blocked_uri: annotated_string_or_none(blocked_uri),
70 source_file: annotated_string_or_none(source_file),
71 document_uri: annotated_string_or_none(document_uri),
72 ..Csp::default()
73 }),
74 ..Event::default()
75 }
76 }
77
78 #[test]
79 fn test_scheme_domain_port_with_ip() {
80 let examples = [
81 (
82 "http://192.168.1.1:3000",
83 Some("http"),
84 Some("192.168.1.1"),
85 Some("3000"),
86 ),
87 ("192.168.1.1", None, Some("192.168.1.1"), None),
88 ("[fd45:7aa3:7ae4::]", None, Some("[fd45:7aa3:7ae4::]"), None),
89 ("http://172.16.*.*", Some("http"), Some("172.16.*.*"), None),
90 (
91 "http://[1fff:0:a88:85a3::ac1f]:8001",
92 Some("http"),
93 Some("[1fff:0:a88:85a3::ac1f]"),
94 Some("8001"),
95 ),
96 ("::1", None, Some(":"), Some("1")),
98 ("[::1]", None, Some("[::1]"), None),
99 (
100 "http://[fe80::862a:fdff:fe78:a2bf%13]",
101 Some("http"),
102 Some("[fe80::862a:fdff:fe78:a2bf%13]"),
103 None,
104 ),
105 ("192.168.1.1.1", None, Some("192.168.1.1.1"), None),
108 ("192.168.1.300", None, Some("192.168.1.300"), None),
109 (
110 "[2001:0db8:85a3:::8a2e:0370:7334]",
111 None,
112 Some("[2001:0db8:85a3:::8a2e:0370:7334]"),
113 None,
114 ),
115 ("[fe80::1::]", None, Some("[fe80::1::]"), None),
116 ("fe80::1::", None, Some("fe80::1:"), Some("")),
117 (
118 "[2001:0db8:85a3:xyz::8a2e:0370:7334]",
119 None,
120 Some("[2001:0db8:85a3:xyz::8a2e:0370:7334]"),
121 None,
122 ),
123 (
124 "2001:0db8:85a3:xyz::8a2e:0370:7334",
125 None,
126 Some("2001:0db8:85a3:xyz::8a2e:0370"),
127 Some("7334"),
128 ),
129 ("192.168.0.1/24", None, Some("192.168.0.1"), None),
130 ];
131
132 for (url, scheme, domain, port) in examples {
133 let actual = SchemeDomainPort::from(url);
134 assert_eq!(
135 (actual.scheme, actual.domain, actual.port),
136 (
137 scheme.map(|x| x.to_owned()),
138 domain.map(|x| x.to_owned()),
139 port.map(|x| x.to_owned())
140 )
141 );
142 }
143 }
144
145 #[test]
146 fn test_filters_known_blocked_source_files() {
147 let event = get_csp_event(None, Some("http://known.bad.com"), None);
148 let config = CspFilterConfig {
149 disallowed_sources: vec!["http://known.bad.com".to_owned()],
150 };
151
152 let actual = should_filter(&event, &config);
153 assert_ne!(
154 actual,
155 Ok(()),
156 "CSP filter should have filtered known bad source file"
157 );
158 }
159
160 #[test]
161 fn test_does_not_filter_benign_source_files() {
162 let event = get_csp_event(None, Some("http://good.file.com"), None);
163 let config = CspFilterConfig {
164 disallowed_sources: vec!["http://known.bad.com".to_owned()],
165 };
166
167 let actual = should_filter(&event, &config);
168 assert_eq!(
169 actual,
170 Ok(()),
171 "CSP filter should have NOT filtered good source file"
172 );
173 }
174
175 #[test]
176 fn test_filters_known_document_uris() {
177 let event = get_csp_event(None, None, Some("http://known.bad.com"));
178 let config = CspFilterConfig {
179 disallowed_sources: vec!["http://known.bad.com".to_owned()],
180 };
181
182 let actual = should_filter(&event, &config);
183 assert_ne!(
184 actual,
185 Ok(()),
186 "CSP filter should have filtered known document uri"
187 );
188 }
189
190 #[test]
191 fn test_filters_known_blocked_uris() {
192 let event = get_csp_event(Some("http://known.bad.com"), None, None);
193 let config = CspFilterConfig {
194 disallowed_sources: vec!["http://known.bad.com".to_owned()],
195 };
196
197 let actual = should_filter(&event, &config);
198 assert_ne!(
199 actual,
200 Ok(()),
201 "CSP filter should have filtered known blocked uri"
202 );
203 }
204
205 #[test]
206 fn test_does_not_filter_benign_uris() {
207 let event = get_csp_event(Some("http://good.file.com"), None, None);
208 let config = CspFilterConfig {
209 disallowed_sources: vec!["http://known.bad.com".to_owned()],
210 };
211
212 let actual = should_filter(&event, &config);
213 assert_eq!(
214 actual,
215 Ok(()),
216 "CSP filter should have NOT filtered unknown blocked uri"
217 );
218 }
219
220 #[test]
221 fn test_does_not_filter_non_csp_messages() {
222 let mut event = get_csp_event(Some("http://known.bad.com"), None, None);
223 event.ty = Annotated::from(EventType::Transaction);
224 let config = CspFilterConfig {
225 disallowed_sources: vec!["http://known.bad.com".to_owned()],
226 };
227
228 let actual = should_filter(&event, &config);
229 assert_eq!(
230 actual,
231 Ok(()),
232 "CSP filter should have NOT filtered non CSP event"
233 );
234 }
235
236 fn get_disallowed_sources() -> Vec<String> {
237 vec![
238 "about".to_owned(),
239 "ms-browser-extension".to_owned(),
240 "*.superfish.com".to_owned(),
241 "chrome://*".to_owned(),
242 "chrome-extension://*".to_owned(),
243 "chromeinvokeimmediate://*".to_owned(),
244 "chromenull://*".to_owned(),
245 "localhost".to_owned(),
246 ]
247 }
248
249 #[test]
251 fn test_sentry_csp_filter_compatibility_bad_reports() {
252 let examples = &[
253 (Some("about"), None),
254 (Some("ms-browser-extension"), None),
255 (Some("http://foo.superfish.com"), None),
256 (None, Some("chrome-extension://fdsa")),
257 (None, Some("http://localhost:8000")),
258 (None, Some("http://localhost")),
259 (None, Some("http://foo.superfish.com")),
260 ];
261
262 for (blocked_uri, source_file) in examples {
263 let event = get_csp_event(*blocked_uri, *source_file, None);
264 let config = CspFilterConfig {
265 disallowed_sources: get_disallowed_sources(),
266 };
267
268 let actual = should_filter(&event, &config);
269 assert_ne!(
270 actual,
271 Ok(()),
272 "CSP filter should have filtered bad request {blocked_uri:?} {source_file:?}"
273 );
274 }
275 }
276
277 #[test]
278 fn test_sentry_csp_filter_compatibility_good_reports() {
279 let examples = &[
280 (Some("http://example.com"), None),
281 (None, Some("http://example.com")),
282 (None, None),
283 ];
284
285 for (blocked_uri, source_file) in examples {
286 let event = get_csp_event(*blocked_uri, *source_file, None);
287 let config = CspFilterConfig {
288 disallowed_sources: get_disallowed_sources(),
289 };
290
291 let actual = should_filter(&event, &config);
292 assert_eq!(
293 actual,
294 Ok(()),
295 "CSP filter should have NOT filtered request {blocked_uri:?} {source_file:?}"
296 );
297 }
298 }
299}