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 if 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 }
159 continue; }
161 return true;
163 }
164 }
165 false
166}
167
168#[cfg(test)]
169mod tests {
170 use relay_event_schema::protocol::{Csp, Event, EventType};
171 use relay_protocol::Annotated;
172
173 use super::*;
174
175 fn get_csp_event(
176 blocked_uri: Option<&str>,
177 source_file: Option<&str>,
178 document_uri: Option<&str>,
179 ) -> Event {
180 fn annotated_string_or_none(val: Option<&str>) -> Annotated<String> {
181 match val {
182 None => Annotated::empty(),
183 Some(val) => Annotated::from(val.to_string()),
184 }
185 }
186 Event {
187 ty: Annotated::from(EventType::Csp),
188 csp: Annotated::from(Csp {
189 blocked_uri: annotated_string_or_none(blocked_uri),
190 source_file: annotated_string_or_none(source_file),
191 document_uri: annotated_string_or_none(document_uri),
192 ..Csp::default()
193 }),
194 ..Event::default()
195 }
196 }
197
198 #[test]
199 fn test_scheme_domain_port() {
200 let examples = &[
201 ("*", None, None, None),
202 ("*://*", None, None, None),
203 ("*://*:*", None, None, None),
204 ("https://*", Some("https"), None, None),
205 ("https://*.abc.net", Some("https"), Some("*.abc.net"), None),
206 ("https://*:*", Some("https"), None, 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 ("*://*.x.y.z:*", None, Some("*.x.y.z"), None),
211 ("*:8000", None, None, Some("8000")),
212 ("*://*:8000", None, None, Some("8000")),
213 ("http://x.y.z", Some("http"), Some("x.y.z"), None),
214 ("http://*:8000", Some("http"), None, Some("8000")),
215 ("abc:8000", None, Some("abc"), Some("8000")),
216 ("*.abc.com:8000", None, Some("*.abc.com"), Some("8000")),
217 ("*.com:86", None, Some("*.com"), Some("86")),
218 (
219 "http://abc.com:86",
220 Some("http"),
221 Some("abc.com"),
222 Some("86"),
223 ),
224 (
225 "http://x.y.z:4000",
226 Some("http"),
227 Some("x.y.z"),
228 Some("4000"),
229 ),
230 ("http://", Some("http"), Some(""), None),
231 ("abc.com/[something]", None, Some("abc.com"), None),
232 ("abc.com/something]:", None, Some("abc.com"), None),
233 ("abc.co]m/[something:", None, Some("abc.co]m"), None),
234 ("]abc.com:9000", None, Some("]abc.com"), Some("9000")),
235 (
236 "https://api.example.com/foo/00000000-0000-0000-0000-000000000000?includes[]=user&includes[]=image&includes[]=author&includes[]=tag",
237 Some("https"),
238 Some("api.example.com"),
239 None,
240 ),
241 ];
242
243 for (url, scheme, domain, port) in examples {
244 let actual: SchemeDomainPort = (*url).into();
245 assert_eq!(
246 (actual.scheme, actual.domain, actual.port),
247 (
248 scheme.map(|x| x.to_string()),
249 domain.map(|x| x.to_string()),
250 port.map(|x| x.to_string())
251 )
252 );
253 }
254 }
255
256 #[test]
257 fn test_scheme_domain_port_with_ip() {
258 let examples = [
259 (
260 "http://192.168.1.1:3000",
261 Some("http"),
262 Some("192.168.1.1"),
263 Some("3000"),
264 ),
265 ("192.168.1.1", None, Some("192.168.1.1"), None),
266 ("[fd45:7aa3:7ae4::]", None, Some("[fd45:7aa3:7ae4::]"), None),
267 ("http://172.16.*.*", Some("http"), Some("172.16.*.*"), None),
268 (
269 "http://[1fff:0:a88:85a3::ac1f]:8001",
270 Some("http"),
271 Some("[1fff:0:a88:85a3::ac1f]"),
272 Some("8001"),
273 ),
274 ("::1", None, Some(":"), Some("1")),
276 ("[::1]", None, Some("[::1]"), None),
277 (
278 "http://[fe80::862a:fdff:fe78:a2bf%13]",
279 Some("http"),
280 Some("[fe80::862a:fdff:fe78:a2bf%13]"),
281 None,
282 ),
283 ("192.168.1.1.1", None, Some("192.168.1.1.1"), None),
286 ("192.168.1.300", None, Some("192.168.1.300"), None),
287 (
288 "[2001:0db8:85a3:::8a2e:0370:7334]",
289 None,
290 Some("[2001:0db8:85a3:::8a2e:0370:7334]"),
291 None,
292 ),
293 ("[fe80::1::]", None, Some("[fe80::1::]"), None),
294 ("fe80::1::", None, Some("fe80::1:"), Some("")),
295 (
296 "[2001:0db8:85a3:xyz::8a2e:0370:7334]",
297 None,
298 Some("[2001:0db8:85a3:xyz::8a2e:0370:7334]"),
299 None,
300 ),
301 (
302 "2001:0db8:85a3:xyz::8a2e:0370:7334",
303 None,
304 Some("2001:0db8:85a3:xyz::8a2e:0370"),
305 Some("7334"),
306 ),
307 ("192.168.0.1/24", None, Some("192.168.0.1"), None),
308 ];
309
310 for (url, scheme, domain, port) in examples {
311 let actual = SchemeDomainPort::from(url);
312 assert_eq!(
313 (actual.scheme, actual.domain, actual.port),
314 (
315 scheme.map(|x| x.to_string()),
316 domain.map(|x| x.to_string()),
317 port.map(|x| x.to_string())
318 )
319 );
320 }
321 }
322
323 #[test]
324 fn test_matches_any_origin() {
325 let examples = &[
326 ("http://abc1.com", vec!["*://*:*", "bbc.com"], true),
329 ("http://abc2.com", vec!["*:*", "bbc.com"], true),
330 ("http://abc3.com", vec!["*", "bbc.com"], true),
331 ("http://abc4.com", vec!["http://*", "bbc.com"], true),
332 (
333 "http://abc5.com",
334 vec!["http://abc5.com:*", "bbc.com"],
335 true,
336 ),
337 ("http://abc.com:80", vec!["*://*:*", "bbc.com"], true),
338 ("http://abc.com:81", vec!["*:*", "bbc.com"], true),
339 ("http://abc.com:82", vec!["*:82", "bbc.com"], true),
340 ("http://abc.com:83", vec!["http://*:83", "bbc.com"], true),
341 ("http://abc.com:84", vec!["abc.com:*", "bbc.com"], true),
342 ("http://abc.com:85", vec!["*.abc.com:85", "bbc.com"], true),
344 ("http://abc.com:86", vec!["*.com:86"], true),
345 ("http://abc.com:86", vec!["*.com:86", "bbc.com"], true),
346 ("http://abc.def.ghc.com:87", vec!["*.com:87"], true),
347 ("http://abc.def.ghc.com:88", vec!["*.ghc.com:88"], true),
348 ("http://abc.def.ghc.com:89", vec!["*.def.ghc.com:89"], true),
349 ("http://abc.com:90", vec!["abc.com", "bbc.com"], true),
351 ("http://abc.com:91", vec!["abc.com:91", "bbc.com"], true),
352 ("http://abc.com:92", vec!["http://abc.com:92"], true),
353 ("http://abc.com:93", vec!["http://abc.com", "bbc.com"], true),
354 ("http://abc6.com", vec!["abc6.com", "bbc.com"], true),
356 ("http://abc7.com", vec!["bbc.com", "abc7.com"], true),
357 ("http://abc8.com", vec!["bbc.com", "abc8.com", "def"], true),
358 (
361 "http://abc9.com",
362 vec!["http://other.com", "bbc.com"],
363 false,
364 ),
365 ("http://abc10.com", vec!["http://*.other.com", "bbc"], false),
366 ("abc11.com", vec!["*.other.com", "bbc"], false),
367 (
369 "https://abc12.com",
370 vec!["http://abc12.com", "bbc.com"],
371 false,
372 ),
373 (
375 "http://abc13.com:80",
376 vec!["http://abc13.com:8080", "bbc.com"],
377 false,
378 ),
379 ("http://y:80", vec!["http://x"], false),
381 (
383 "https://abc.software.example.com",
384 vec!["*abc.software.example.com*"],
385 false,
386 ),
387 ];
388
389 for (url, origins, expected) in examples {
390 let origins: Vec<_> = origins
391 .iter()
392 .map(|url| SchemeDomainPort::from(*url))
393 .collect();
394 let actual = matches_any_origin(Some(*url), &origins[..]);
395 assert_eq!(*expected, actual, "Could not match {url}.");
396 }
397 }
398
399 #[test]
400 fn test_filters_known_blocked_source_files() {
401 let event = get_csp_event(None, Some("http://known.bad.com"), None);
402 let config = CspFilterConfig {
403 disallowed_sources: vec!["http://known.bad.com".to_string()],
404 };
405
406 let actual = should_filter(&event, &config);
407 assert_ne!(
408 actual,
409 Ok(()),
410 "CSP filter should have filtered known bad source file"
411 );
412 }
413
414 #[test]
415 fn test_does_not_filter_benign_source_files() {
416 let event = get_csp_event(None, Some("http://good.file.com"), None);
417 let config = CspFilterConfig {
418 disallowed_sources: vec!["http://known.bad.com".to_string()],
419 };
420
421 let actual = should_filter(&event, &config);
422 assert_eq!(
423 actual,
424 Ok(()),
425 "CSP filter should have NOT filtered good source file"
426 );
427 }
428
429 #[test]
430 fn test_filters_known_document_uris() {
431 let event = get_csp_event(None, None, Some("http://known.bad.com"));
432 let config = CspFilterConfig {
433 disallowed_sources: vec!["http://known.bad.com".to_string()],
434 };
435
436 let actual = should_filter(&event, &config);
437 assert_ne!(
438 actual,
439 Ok(()),
440 "CSP filter should have filtered known document uri"
441 );
442 }
443
444 #[test]
445 fn test_filters_known_blocked_uris() {
446 let event = get_csp_event(Some("http://known.bad.com"), None, None);
447 let config = CspFilterConfig {
448 disallowed_sources: vec!["http://known.bad.com".to_string()],
449 };
450
451 let actual = should_filter(&event, &config);
452 assert_ne!(
453 actual,
454 Ok(()),
455 "CSP filter should have filtered known blocked uri"
456 );
457 }
458
459 #[test]
460 fn test_does_not_filter_benign_uris() {
461 let event = get_csp_event(Some("http://good.file.com"), None, None);
462 let config = CspFilterConfig {
463 disallowed_sources: vec!["http://known.bad.com".to_string()],
464 };
465
466 let actual = should_filter(&event, &config);
467 assert_eq!(
468 actual,
469 Ok(()),
470 "CSP filter should have NOT filtered unknown blocked uri"
471 );
472 }
473
474 #[test]
475 fn test_does_not_filter_non_csp_messages() {
476 let mut event = get_csp_event(Some("http://known.bad.com"), None, None);
477 event.ty = Annotated::from(EventType::Transaction);
478 let config = CspFilterConfig {
479 disallowed_sources: vec!["http://known.bad.com".to_string()],
480 };
481
482 let actual = should_filter(&event, &config);
483 assert_eq!(
484 actual,
485 Ok(()),
486 "CSP filter should have NOT filtered non CSP event"
487 );
488 }
489
490 fn get_disallowed_sources() -> Vec<String> {
491 vec![
492 "about".to_string(),
493 "ms-browser-extension".to_string(),
494 "*.superfish.com".to_string(),
495 "chrome://*".to_string(),
496 "chrome-extension://*".to_string(),
497 "chromeinvokeimmediate://*".to_string(),
498 "chromenull://*".to_string(),
499 "localhost".to_string(),
500 ]
501 }
502
503 #[test]
505 fn test_sentry_csp_filter_compatibility_bad_reports() {
506 let examples = &[
507 (Some("about"), None),
508 (Some("ms-browser-extension"), None),
509 (Some("http://foo.superfish.com"), None),
510 (None, Some("chrome-extension://fdsa")),
511 (None, Some("http://localhost:8000")),
512 (None, Some("http://localhost")),
513 (None, Some("http://foo.superfish.com")),
514 ];
515
516 for (blocked_uri, source_file) in examples {
517 let event = get_csp_event(*blocked_uri, *source_file, None);
518 let config = CspFilterConfig {
519 disallowed_sources: get_disallowed_sources(),
520 };
521
522 let actual = should_filter(&event, &config);
523 assert_ne!(
524 actual,
525 Ok(()),
526 "CSP filter should have filtered bad request {blocked_uri:?} {source_file:?}"
527 );
528 }
529 }
530
531 #[test]
532 fn test_sentry_csp_filter_compatibility_good_reports() {
533 let examples = &[
534 (Some("http://example.com"), None),
535 (None, Some("http://example.com")),
536 (None, None),
537 ];
538
539 for (blocked_uri, source_file) in examples {
540 let event = get_csp_event(*blocked_uri, *source_file, None);
541 let config = CspFilterConfig {
542 disallowed_sources: get_disallowed_sources(),
543 };
544
545 let actual = should_filter(&event, &config);
546 assert_eq!(
547 actual,
548 Ok(()),
549 "CSP filter should have NOT filtered request {blocked_uri:?} {source_file:?}"
550 );
551 }
552 }
553}