1use relay_event_schema::protocol::Csp;
6
7use crate::{CspFilterConfig, FilterStatKey, Filterable};
8
9fn matches<It, S>(csp: Option<&Csp>, disallowed_sources: It) -> bool
11where
12 It: IntoIterator<Item = S>,
13 S: AsRef<str>,
14{
15 let disallowed_sources: Vec<SchemeDomainPort> = disallowed_sources
17 .into_iter()
18 .map(|origin| -> SchemeDomainPort { origin.as_ref().into() })
19 .collect();
20
21 if let Some(csp) = csp {
22 if matches_any_origin(csp.blocked_uri.as_str(), &disallowed_sources) {
23 return true;
24 }
25 if matches_any_origin(csp.source_file.as_str(), &disallowed_sources) {
26 return true;
27 }
28 if matches_any_origin(csp.document_uri.as_str(), &disallowed_sources) {
29 return true;
30 }
31 }
32 false
33}
34
35pub fn should_filter<F>(item: &F, config: &CspFilterConfig) -> Result<(), FilterStatKey>
37where
38 F: Filterable,
39{
40 if matches(item.csp(), &config.disallowed_sources) {
41 Err(FilterStatKey::InvalidCsp)
42 } else {
43 Ok(())
44 }
45}
46
47#[derive(Hash, PartialEq, Eq)]
53pub struct SchemeDomainPort {
54 pub scheme: Option<String>,
56 pub domain: Option<String>,
58 pub port: Option<String>,
60}
61
62impl From<&str> for SchemeDomainPort {
63 fn from(url: &str) -> SchemeDomainPort {
65 fn normalize(pattern: &str) -> Option<String> {
69 if pattern == "*" {
70 None
71 } else {
72 Some(pattern.to_lowercase())
73 }
74 }
75
76 let scheme_idx = url.find("://");
78 let (scheme, rest) = if let Some(idx) = scheme_idx {
79 (normalize(&url[..idx]), &url[idx + 3..]) } else {
81 (None, url) };
83
84 let end_domain_idx = rest.find('/');
86 let domain_port = if let Some(end_domain_idx) = end_domain_idx {
87 &rest[..end_domain_idx] } else {
89 rest };
91
92 let ipv6_end_bracket_idx = domain_port.rfind(']');
94 let port_separator_idx = if let Some(end_bracket_idx) = ipv6_end_bracket_idx {
95 domain_port[end_bracket_idx..]
97 .rfind(':')
98 .map(|x| x + end_bracket_idx)
99 } else {
100 domain_port.rfind(':')
102 };
103 let (domain, port) = if let Some(port_separator_idx) = port_separator_idx {
104 (
106 normalize(&domain_port[..port_separator_idx]),
107 normalize(&domain_port[port_separator_idx + 1..]),
108 )
109 } else {
110 (normalize(domain_port), None) };
112
113 SchemeDomainPort {
114 scheme,
115 domain,
116 port,
117 }
118 }
119}
120
121pub fn matches_any_origin(url: Option<&str>, origins: &[SchemeDomainPort]) -> bool {
131 if origins
133 .iter()
134 .any(|o| o.scheme.is_none() && o.port.is_none() && o.domain.is_none())
135 {
136 return true;
137 }
138
139 if let Some(url) = url {
140 let url = SchemeDomainPort::from(url);
141
142 for origin in origins {
143 if origin.scheme.is_some() && url.scheme != origin.scheme {
144 continue; }
146 if origin.port.is_some() && url.port != origin.port {
147 continue; }
149 if origin.domain.is_some() && url.domain != origin.domain {
150 if let (Some(origin_domain), Some(domain)) = (&origin.domain, &url.domain)
152 && origin_domain.starts_with('*')
153 && ((*domain).ends_with(origin_domain.get(1..).unwrap_or(""))
154 || domain.as_str() == origin_domain.get(2..).unwrap_or(""))
155 {
156 return true; }
158 continue; }
160 return true;
162 }
163 }
164 false
165}
166
167#[cfg(test)]
168mod tests {
169 use relay_event_schema::protocol::{Csp, Event, EventType};
170 use relay_protocol::Annotated;
171
172 use super::*;
173
174 fn get_csp_event(
175 blocked_uri: Option<&str>,
176 source_file: Option<&str>,
177 document_uri: Option<&str>,
178 ) -> Event {
179 fn annotated_string_or_none(val: Option<&str>) -> Annotated<String> {
180 match val {
181 None => Annotated::empty(),
182 Some(val) => Annotated::from(val.to_owned()),
183 }
184 }
185 Event {
186 ty: Annotated::from(EventType::Csp),
187 csp: Annotated::from(Csp {
188 blocked_uri: annotated_string_or_none(blocked_uri),
189 source_file: annotated_string_or_none(source_file),
190 document_uri: annotated_string_or_none(document_uri),
191 ..Csp::default()
192 }),
193 ..Event::default()
194 }
195 }
196
197 #[test]
198 fn test_scheme_domain_port() {
199 let examples = &[
200 ("*", None, None, None),
201 ("*://*", None, None, None),
202 ("*://*:*", None, None, None),
203 ("https://*", Some("https"), None, None),
204 ("https://*.abc.net", Some("https"), Some("*.abc.net"), None),
205 ("https://*:*", Some("https"), None, None),
206 ("x.y.z", None, Some("x.y.z"), None),
207 ("x.y.z:*", None, Some("x.y.z"), None),
208 ("*://x.y.z:*", None, Some("x.y.z"), None),
209 ("*://*.x.y.z:*", None, Some("*.x.y.z"), None),
210 ("*:8000", None, None, Some("8000")),
211 ("*://*:8000", None, None, Some("8000")),
212 ("http://x.y.z", Some("http"), Some("x.y.z"), None),
213 ("http://*:8000", Some("http"), None, Some("8000")),
214 ("abc:8000", None, Some("abc"), Some("8000")),
215 ("*.abc.com:8000", None, Some("*.abc.com"), Some("8000")),
216 ("*.com:86", None, Some("*.com"), Some("86")),
217 (
218 "http://abc.com:86",
219 Some("http"),
220 Some("abc.com"),
221 Some("86"),
222 ),
223 (
224 "http://x.y.z:4000",
225 Some("http"),
226 Some("x.y.z"),
227 Some("4000"),
228 ),
229 ("http://", Some("http"), Some(""), None),
230 ("abc.com/[something]", None, Some("abc.com"), None),
231 ("abc.com/something]:", None, Some("abc.com"), None),
232 ("abc.co]m/[something:", None, Some("abc.co]m"), None),
233 ("]abc.com:9000", None, Some("]abc.com"), Some("9000")),
234 (
235 "https://api.example.com/foo/00000000-0000-0000-0000-000000000000?includes[]=user&includes[]=image&includes[]=author&includes[]=tag",
236 Some("https"),
237 Some("api.example.com"),
238 None,
239 ),
240 ];
241
242 for (url, scheme, domain, port) in examples {
243 let actual: SchemeDomainPort = (*url).into();
244 assert_eq!(
245 (actual.scheme, actual.domain, actual.port),
246 (
247 scheme.map(|x| x.to_owned()),
248 domain.map(|x| x.to_owned()),
249 port.map(|x| x.to_owned())
250 )
251 );
252 }
253 }
254
255 #[test]
256 fn test_scheme_domain_port_with_ip() {
257 let examples = [
258 (
259 "http://192.168.1.1:3000",
260 Some("http"),
261 Some("192.168.1.1"),
262 Some("3000"),
263 ),
264 ("192.168.1.1", None, Some("192.168.1.1"), None),
265 ("[fd45:7aa3:7ae4::]", None, Some("[fd45:7aa3:7ae4::]"), None),
266 ("http://172.16.*.*", Some("http"), Some("172.16.*.*"), None),
267 (
268 "http://[1fff:0:a88:85a3::ac1f]:8001",
269 Some("http"),
270 Some("[1fff:0:a88:85a3::ac1f]"),
271 Some("8001"),
272 ),
273 ("::1", None, Some(":"), Some("1")),
275 ("[::1]", None, Some("[::1]"), None),
276 (
277 "http://[fe80::862a:fdff:fe78:a2bf%13]",
278 Some("http"),
279 Some("[fe80::862a:fdff:fe78:a2bf%13]"),
280 None,
281 ),
282 ("192.168.1.1.1", None, Some("192.168.1.1.1"), None),
285 ("192.168.1.300", None, Some("192.168.1.300"), None),
286 (
287 "[2001:0db8:85a3:::8a2e:0370:7334]",
288 None,
289 Some("[2001:0db8:85a3:::8a2e:0370:7334]"),
290 None,
291 ),
292 ("[fe80::1::]", None, Some("[fe80::1::]"), None),
293 ("fe80::1::", None, Some("fe80::1:"), Some("")),
294 (
295 "[2001:0db8:85a3:xyz::8a2e:0370:7334]",
296 None,
297 Some("[2001:0db8:85a3:xyz::8a2e:0370:7334]"),
298 None,
299 ),
300 (
301 "2001:0db8:85a3:xyz::8a2e:0370:7334",
302 None,
303 Some("2001:0db8:85a3:xyz::8a2e:0370"),
304 Some("7334"),
305 ),
306 ("192.168.0.1/24", None, Some("192.168.0.1"), None),
307 ];
308
309 for (url, scheme, domain, port) in examples {
310 let actual = SchemeDomainPort::from(url);
311 assert_eq!(
312 (actual.scheme, actual.domain, actual.port),
313 (
314 scheme.map(|x| x.to_owned()),
315 domain.map(|x| x.to_owned()),
316 port.map(|x| x.to_owned())
317 )
318 );
319 }
320 }
321
322 #[test]
323 fn test_matches_any_origin() {
324 let examples = &[
325 ("http://abc1.com", vec!["*://*:*", "bbc.com"], true),
328 ("http://abc2.com", vec!["*:*", "bbc.com"], true),
329 ("http://abc3.com", vec!["*", "bbc.com"], true),
330 ("http://abc4.com", vec!["http://*", "bbc.com"], true),
331 (
332 "http://abc5.com",
333 vec!["http://abc5.com:*", "bbc.com"],
334 true,
335 ),
336 ("http://abc.com:80", vec!["*://*:*", "bbc.com"], true),
337 ("http://abc.com:81", vec!["*:*", "bbc.com"], true),
338 ("http://abc.com:82", vec!["*:82", "bbc.com"], true),
339 ("http://abc.com:83", vec!["http://*:83", "bbc.com"], true),
340 ("http://abc.com:84", vec!["abc.com:*", "bbc.com"], true),
341 ("http://abc.com:85", vec!["*.abc.com:85", "bbc.com"], true),
343 ("http://abc.com:86", vec!["*.com:86"], true),
344 ("http://abc.com:86", vec!["*.com:86", "bbc.com"], true),
345 ("http://abc.def.ghc.com:87", vec!["*.com:87"], true),
346 ("http://abc.def.ghc.com:88", vec!["*.ghc.com:88"], true),
347 ("http://abc.def.ghc.com:89", vec!["*.def.ghc.com:89"], true),
348 ("http://abc.com:90", vec!["abc.com", "bbc.com"], true),
350 ("http://abc.com:91", vec!["abc.com:91", "bbc.com"], true),
351 ("http://abc.com:92", vec!["http://abc.com:92"], true),
352 ("http://abc.com:93", vec!["http://abc.com", "bbc.com"], true),
353 ("http://abc6.com", vec!["abc6.com", "bbc.com"], true),
355 ("http://abc7.com", vec!["bbc.com", "abc7.com"], true),
356 ("http://abc8.com", vec!["bbc.com", "abc8.com", "def"], true),
357 (
360 "http://abc9.com",
361 vec!["http://other.com", "bbc.com"],
362 false,
363 ),
364 ("http://abc10.com", vec!["http://*.other.com", "bbc"], false),
365 ("abc11.com", vec!["*.other.com", "bbc"], false),
366 (
368 "https://abc12.com",
369 vec!["http://abc12.com", "bbc.com"],
370 false,
371 ),
372 (
374 "http://abc13.com:80",
375 vec!["http://abc13.com:8080", "bbc.com"],
376 false,
377 ),
378 ("http://y:80", vec!["http://x"], false),
380 (
382 "https://abc.software.example.com",
383 vec!["*abc.software.example.com*"],
384 false,
385 ),
386 ];
387
388 for (url, origins, expected) in examples {
389 let origins: Vec<_> = origins
390 .iter()
391 .map(|url| SchemeDomainPort::from(*url))
392 .collect();
393 let actual = matches_any_origin(Some(*url), &origins[..]);
394 assert_eq!(*expected, actual, "Could not match {url}.");
395 }
396 }
397
398 #[test]
399 fn test_filters_known_blocked_source_files() {
400 let event = get_csp_event(None, Some("http://known.bad.com"), None);
401 let config = CspFilterConfig {
402 disallowed_sources: vec!["http://known.bad.com".to_owned()],
403 };
404
405 let actual = should_filter(&event, &config);
406 assert_ne!(
407 actual,
408 Ok(()),
409 "CSP filter should have filtered known bad source file"
410 );
411 }
412
413 #[test]
414 fn test_does_not_filter_benign_source_files() {
415 let event = get_csp_event(None, Some("http://good.file.com"), None);
416 let config = CspFilterConfig {
417 disallowed_sources: vec!["http://known.bad.com".to_owned()],
418 };
419
420 let actual = should_filter(&event, &config);
421 assert_eq!(
422 actual,
423 Ok(()),
424 "CSP filter should have NOT filtered good source file"
425 );
426 }
427
428 #[test]
429 fn test_filters_known_document_uris() {
430 let event = get_csp_event(None, None, Some("http://known.bad.com"));
431 let config = CspFilterConfig {
432 disallowed_sources: vec!["http://known.bad.com".to_owned()],
433 };
434
435 let actual = should_filter(&event, &config);
436 assert_ne!(
437 actual,
438 Ok(()),
439 "CSP filter should have filtered known document uri"
440 );
441 }
442
443 #[test]
444 fn test_filters_known_blocked_uris() {
445 let event = get_csp_event(Some("http://known.bad.com"), None, None);
446 let config = CspFilterConfig {
447 disallowed_sources: vec!["http://known.bad.com".to_owned()],
448 };
449
450 let actual = should_filter(&event, &config);
451 assert_ne!(
452 actual,
453 Ok(()),
454 "CSP filter should have filtered known blocked uri"
455 );
456 }
457
458 #[test]
459 fn test_does_not_filter_benign_uris() {
460 let event = get_csp_event(Some("http://good.file.com"), None, None);
461 let config = CspFilterConfig {
462 disallowed_sources: vec!["http://known.bad.com".to_owned()],
463 };
464
465 let actual = should_filter(&event, &config);
466 assert_eq!(
467 actual,
468 Ok(()),
469 "CSP filter should have NOT filtered unknown blocked uri"
470 );
471 }
472
473 #[test]
474 fn test_does_not_filter_non_csp_messages() {
475 let mut event = get_csp_event(Some("http://known.bad.com"), None, None);
476 event.ty = Annotated::from(EventType::Transaction);
477 let config = CspFilterConfig {
478 disallowed_sources: vec!["http://known.bad.com".to_owned()],
479 };
480
481 let actual = should_filter(&event, &config);
482 assert_eq!(
483 actual,
484 Ok(()),
485 "CSP filter should have NOT filtered non CSP event"
486 );
487 }
488
489 fn get_disallowed_sources() -> Vec<String> {
490 vec![
491 "about".to_owned(),
492 "ms-browser-extension".to_owned(),
493 "*.superfish.com".to_owned(),
494 "chrome://*".to_owned(),
495 "chrome-extension://*".to_owned(),
496 "chromeinvokeimmediate://*".to_owned(),
497 "chromenull://*".to_owned(),
498 "localhost".to_owned(),
499 ]
500 }
501
502 #[test]
504 fn test_sentry_csp_filter_compatibility_bad_reports() {
505 let examples = &[
506 (Some("about"), None),
507 (Some("ms-browser-extension"), None),
508 (Some("http://foo.superfish.com"), None),
509 (None, Some("chrome-extension://fdsa")),
510 (None, Some("http://localhost:8000")),
511 (None, Some("http://localhost")),
512 (None, Some("http://foo.superfish.com")),
513 ];
514
515 for (blocked_uri, source_file) in examples {
516 let event = get_csp_event(*blocked_uri, *source_file, None);
517 let config = CspFilterConfig {
518 disallowed_sources: get_disallowed_sources(),
519 };
520
521 let actual = should_filter(&event, &config);
522 assert_ne!(
523 actual,
524 Ok(()),
525 "CSP filter should have filtered bad request {blocked_uri:?} {source_file:?}"
526 );
527 }
528 }
529
530 #[test]
531 fn test_sentry_csp_filter_compatibility_good_reports() {
532 let examples = &[
533 (Some("http://example.com"), None),
534 (None, Some("http://example.com")),
535 (None, None),
536 ];
537
538 for (blocked_uri, source_file) in examples {
539 let event = get_csp_event(*blocked_uri, *source_file, None);
540 let config = CspFilterConfig {
541 disallowed_sources: get_disallowed_sources(),
542 };
543
544 let actual = should_filter(&event, &config);
545 assert_eq!(
546 actual,
547 Ok(()),
548 "CSP filter should have NOT filtered request {blocked_uri:?} {source_file:?}"
549 );
550 }
551 }
552}