Skip to main content

relay_filter/
csp.rs

1//! Implements event filtering for events originating from CSP endpoints
2//!
3//! Events originating from a CSP message can be filtered based on the source URL
4
5use relay_event_schema::protocol::Csp;
6
7use crate::origin::{SchemeDomainPort, matches_any_origin};
8use crate::{CspFilterConfig, FilterStatKey, Filterable};
9
10/// Checks if the event is a CSP Event from one of the disallowed sources.
11fn matches<It, S>(csp: Option<&Csp>, disallowed_sources: It) -> bool
12where
13    It: IntoIterator<Item = S>,
14    S: AsRef<str>,
15{
16    // parse the sources for easy processing
17    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
36/// Filters CSP events based on disallowed sources.
37pub 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            // invalid IPv6 for localhost since it's not inside brackets
97            ("::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            // invalid addresses. although these results don't represent correct results,
106            // they are here to make sure the application won't crash.
107            ("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    /// Copy the test cases from Sentry
250    #[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}