Skip to main content

relay_pii/
regexes.rs

1use std::sync::LazyLock;
2
3use regex::Regex;
4use smallvec::{SmallVec, smallvec};
5
6use crate::config::RuleType;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
9pub enum PatternType {
10    /// Pattern-match on key and value
11    KeyValue,
12    /// Pattern-match on key only, apply replacement on the value.
13    Key,
14    /// Pattern-match on value only
15    Value,
16}
17
18/// What to do with regex matches once found.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub enum ReplaceBehavior {
21    /// Replace the entire string value (or just more than the match, depending on context).
22    Value,
23
24    /// Replace the following specific regex groups.
25    Groups(SmallVec<[u8; 1]>),
26}
27
28impl ReplaceBehavior {
29    /// Replace the entire string value (or just more than the match, depending on context).
30    pub fn replace_value() -> Self {
31        ReplaceBehavior::Value
32    }
33
34    /// Replace the entire match, equivalent to `ReplaceBehavior::Groups([0])`.
35    pub fn replace_match() -> Self {
36        ReplaceBehavior::replace_group(0)
37    }
38
39    /// Replace the following singular regex group.
40    pub fn replace_group(g: u8) -> Self {
41        ReplaceBehavior::Groups(smallvec![g])
42    }
43
44    /// Replace the following specific regex groups.
45    pub fn replace_groups(gs: SmallVec<[u8; 1]>) -> Self {
46        ReplaceBehavior::Groups(gs)
47    }
48}
49
50/// Return a list of regexes to apply for the given rule type.
51pub fn get_regex_for_rule_type(
52    ty: &RuleType,
53) -> SmallVec<[(PatternType, &Regex, ReplaceBehavior); 2]> {
54    let v = PatternType::Value;
55    let k = PatternType::Key;
56    let kv = PatternType::KeyValue;
57
58    match ty {
59        RuleType::RedactPair(redact_pair) => {
60            if let Ok(pattern) = redact_pair.key_pattern.compiled() {
61                smallvec![(kv, pattern, ReplaceBehavior::replace_value())]
62            } else {
63                smallvec![]
64            }
65        }
66        RuleType::Bearer => {
67            smallvec![(v, &*BEARER_TOKEN_REGEX, ReplaceBehavior::replace_match())]
68        }
69        RuleType::Password => {
70            smallvec![
71                // Bearer token was moved to its own regest and type out of the passwords, but we
72                // still keep it here for backwards compatibility.
73                (v, &*BEARER_TOKEN_REGEX, ReplaceBehavior::replace_match()),
74                (k, &*TOKEN_KEY_REGEX, ReplaceBehavior::replace_value()),
75                (kv, &*PASSWORD_KEY_REGEX, ReplaceBehavior::replace_value()),
76            ]
77        }
78        RuleType::Anything => smallvec![(v, &*ANYTHING_REGEX, ReplaceBehavior::replace_match())],
79        RuleType::Pattern(r) => {
80            let replace_behavior = match r.replace_groups {
81                Some(ref groups) => {
82                    ReplaceBehavior::replace_groups(groups.iter().copied().collect())
83                }
84                None => ReplaceBehavior::replace_match(),
85            };
86            if let Ok(pattern) = r.pattern.compiled() {
87                smallvec![(v, pattern, replace_behavior)]
88            } else {
89                smallvec![]
90            }
91        }
92
93        RuleType::Imei => smallvec![(v, &*IMEI_REGEX, ReplaceBehavior::replace_match())],
94        RuleType::Mac => smallvec![(v, &*MAC_REGEX, ReplaceBehavior::replace_match())],
95        RuleType::Uuid => smallvec![(v, &*UUID_REGEX, ReplaceBehavior::replace_match())],
96        RuleType::Email => smallvec![(v, &*EMAIL_REGEX, ReplaceBehavior::replace_match())],
97        RuleType::Iban => smallvec![(v, &*IBAN_REGEX, ReplaceBehavior::replace_match())],
98        RuleType::Ip => smallvec![
99            (v, &*IPV4_REGEX, ReplaceBehavior::replace_match()),
100            (v, &*IPV6_REGEX, ReplaceBehavior::replace_group(1)),
101        ],
102        RuleType::Creditcard => {
103            smallvec![(v, &*CREDITCARD_REGEX, ReplaceBehavior::replace_match())]
104        }
105        RuleType::Pemkey => smallvec![(v, &*PEM_KEY_REGEX, ReplaceBehavior::replace_group(1))],
106        RuleType::UrlAuth => smallvec![(v, &*URL_AUTH_REGEX, ReplaceBehavior::replace_group(1))],
107        RuleType::UsSsn => smallvec![(v, &*US_SSN_REGEX, ReplaceBehavior::replace_match())],
108        RuleType::Userpath => smallvec![(v, &*PATH_REGEX, ReplaceBehavior::replace_group(1))],
109        RuleType::Cookies => smallvec![(kv, &*COOKIE_REGEX, ReplaceBehavior::replace_group(1))],
110
111        // These ought to have been resolved in CompiledConfig
112        RuleType::Alias(_) | RuleType::Multiple(_) | RuleType::Unknown(_) => smallvec![],
113    }
114}
115
116#[rustfmt::skip]
117macro_rules! ip {
118    (v4s) => { "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" };
119    (v4a) => { concat!(ip!(v4s), "\\.", ip!(v4s), "\\.", ip!(v4s), "\\.", ip!(v4s)) };
120    (v6s) => { "[0-9a-fA-F]{1,4}" };
121}
122
123macro_rules! regex {
124    ($name:ident, $rule:expr) => {
125        #[allow(non_snake_case)]
126        mod $name {
127            use super::*;
128            pub static $name: LazyLock<Regex> = LazyLock::new(|| Regex::new($rule).unwrap());
129
130            #[test]
131            fn supports_byte_mode() {
132                assert!(
133                    regex::bytes::RegexBuilder::new($name.as_str())
134                        .unicode(false)
135                        .multi_line(false)
136                        .dot_matches_new_line(true)
137                        .build()
138                        .is_ok()
139                );
140            }
141        }
142        use $name::$name;
143    };
144}
145
146pub static ANYTHING_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(".*").unwrap());
147
148pub static SENSITIVE_COOKIE_NAMES: &[&str] = &[
149    // Common session cookie names for popular web frameworks
150    "sentrysid", // Sentry default session cookie name
151    "sudo",      // Sentry default sudo cookie name
152    "su",        // Sentry superuser cookie name
153    "session",
154    "__session",
155    "sessionid",
156    "user_session",
157    "symfony",
158    "phpsessid",
159    "fasthttpsessionid",
160    "mysession",
161    "irissessionid",
162    "_vercel_jwt",
163    "connect.sid",
164    "JSESSIONID",
165    "ASP.NET_SessionId",
166    "laravel_session",
167    // Common CSRF/XSRF cookie names for popular web frameworks
168    "csrf",
169    "xsrf",
170    "_xsrf",
171    "_csrf",
172    "csrf-token",
173    "csrf_token",
174    "xsrf-token",
175    "xsrf_token",
176    "fastcsrf",
177    "_iris_csrf",
178];
179
180static COOKIE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
181    let variants = SENSITIVE_COOKIE_NAMES.join("|").replace('.', "\\.");
182    let r = format!("(?i)\\b(?:(?:{variants})(?:__\\d+)?)=([^;]+)");
183    Regex::new(&r).unwrap()
184});
185
186regex!(
187    IMEI_REGEX,
188    r"(?x)
189        \b
190            (\d{2}-?
191                \d{6}-?
192                \d{6}-?
193                \d{1,2})
194        \b
195    "
196);
197
198regex!(
199    MAC_REGEX,
200    r"(?x)
201        \b([[:xdigit:]]{2}[:-]){5}[[:xdigit:]]{2}\b
202    "
203);
204
205regex!(
206    UUID_REGEX,
207    r"(?ix)
208        \b
209        [a-z0-9]{8}-?
210        [a-z0-9]{4}-?
211        [a-z0-9]{4}-?
212        [a-z0-9]{4}-?
213        [a-z0-9]{12}
214        \b
215    "
216);
217
218regex!(
219    EMAIL_REGEX,
220    r"(?x)
221        \b
222            [a-zA-Z0-9.!\#$%&'*+/=?^_`{|}~-]+
223            @
224            [a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}
225        \b
226    "
227);
228
229regex!(
230    IBAN_REGEX,
231    r"(?x)
232        \b
233        (AT|AD|AE|AL|AZ|BA|BE|BG|BH|BR|BY|CH|CR|CY|CZ|DE|DK|DO|EE|EG|ES|FI|FO|FR|GB|GE|GI|GL|GR|GT|HR|HU|IE|IL|IQ|IS|IT|JO|KW|KZ|LB|LC|LI|LT|LU|LV|LY|MC|MD|ME|MK|MR|MT|MU|NL|NO|PK|PL|PS|PT|QA|RO|RU|RS|SA|SC|SE|SI|SK|SM|ST|SV|TL|TN|TR|UA|VA|VG|XK|DZ|AO|BJ|BF|BI|CV|CM|CF|TD|KM|CG|CI|DJ|GQ|GA|GW|HN|IR|MG|ML|MA|MZ|NI|NE|SN|TG)\d{2}[a-zA-Z0-9]{11,29}
234        \b
235    "
236);
237
238regex!(IPV4_REGEX, concat!("\\b", ip!(v4a), "\\b"));
239
240regex!(
241    IPV6_REGEX,
242    concat!(
243        "(?i)(?:[\\s]|[[:punct:]]|^)(",
244        "(",
245        ip!(v6s),
246        ":){7}",
247        ip!(v6s),
248        "|",
249        "(",
250        ip!(v6s),
251        ":){1,7}:|",
252        "(",
253        ip!(v6s),
254        ":){1,6}::",
255        ip!(v6s),
256        "|",
257        "(",
258        ip!(v6s),
259        ":){1,5}:(:",
260        ip!(v6s),
261        "){1,2}|",
262        "(",
263        ip!(v6s),
264        ":){1,4}:(:",
265        ip!(v6s),
266        "){1,3}|",
267        "(",
268        ip!(v6s),
269        ":){1,3}:(:",
270        ip!(v6s),
271        "){1,4}|",
272        "(",
273        ip!(v6s),
274        ":){1,2}:(:",
275        ip!(v6s),
276        "){1,5}|",
277        ip!(v6s),
278        ":((:",
279        ip!(v6s),
280        "){1,6})|",
281        ":((:",
282        ip!(v6s),
283        "){1,7}|:)|",
284        "fe80:(:",
285        ip!(v6s),
286        "){0,4}%[0-9a-zA-Z]{1,}",
287        "::(ffff(:0{1,4}){0,1}:){0,1}",
288        ip!(v4a),
289        "|",
290        "(",
291        ip!(v6s),
292        ":){1,4}:",
293        ip!(v4a),
294        ")([\\s]|[[:punct:]]|$)",
295    )
296);
297
298// http://www.richardsramblings.com/regex/credit-card-numbers/
299// Re-formatted with comments and dashes support
300//
301// Why so complicated? Because creditcard numbers are variable length and we do not want to
302// strip any number that just happens to have the same length.
303regex!(
304    CREDITCARD_REGEX,
305    r#"(?x)
306    \b(
307        (?:  # vendor specific prefixes
308                3[47]\d      # amex (no 13-digit version) (length: 15)
309            | 4\d{3}       # visa (16-digit version only)
310            | 5[1-5]\d\d   # mastercard
311            | 65\d\d       # discover network (subset)
312            | 6011         # discover network (subset)
313        )
314
315        # "wildcard" remainder (allowing dashes in every position because of variable length)
316        ([-\s]?\d){12}
317    )\b
318    "#
319);
320
321regex!(
322    PATH_REGEX,
323    r"(?ix)
324        (?:
325            (?:
326                \b(?:[a-zA-Z]:[\\/])?
327                (?:users|home|documents and settings|[^/\\]+[/\\]profiles)[\\/]
328            ) | (?:
329                /(?:home|users)/
330            )
331        )
332        (
333            [^/\\\r\n\x00]+
334        )
335    "
336);
337
338regex!(
339    PEM_KEY_REGEX,
340    r"(?sx)
341        (?:
342            -----
343            BEGIN[A-Z\ ]+(?:PRIVATE|PUBLIC)\ KEY
344            -----
345            [\t\ ]*\r?\n?
346        )
347        (.+?)
348        (?:
349            \r?\n?
350            -----
351            END[A-Z\ ]+(?:PRIVATE|PUBLIC)\ KEY
352            -----
353        )
354    "
355);
356
357regex!(
358    URL_AUTH_REGEX,
359    r"(?x)
360        \b(?:
361            (?:[a-z0-9+-]+:)?//
362            ([a-zA-Z0-9%_.-]+(?::[a-zA-Z0-9%_.-]+)?)
363        )@
364    "
365);
366
367regex!(
368    US_SSN_REGEX,
369    r"(?x)
370        \b(
371            [0-9]{3}-
372            [0-9]{2}-
373            [0-9]{4}
374        )\b
375    "
376);
377
378regex!(BEARER_TOKEN_REGEX, r"(?i)\b(Bearer\s+)([^\s]+)");
379
380regex!(TOKEN_KEY_REGEX, r"(?i)(token)");
381
382regex!(
383    PASSWORD_KEY_REGEX,
384    r"(?i)(password|secret|passwd|api[-_]key|apikey|auth|credentials|mysql_pwd|privatekey|private[-_]key|token[^\s]*[:=]|^otp$|^two[-_]factor$)"
385);
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_userpath_utf8_bytes() {
393        // This mimicks `apply_regex_to_utf8_bytes`, which is used in minidump scrubbing.
394        // Ideally we would not compile a regex on the fly for every minidump
395        // (either add another lazy static or remove the distinction entirely).
396        let regex = regex::bytes::RegexBuilder::new(PATH_REGEX.as_str())
397            .unicode(false)
398            .multi_line(false)
399            .dot_matches_new_line(true)
400            .build()
401            .unwrap();
402        assert!(regex.is_match(br"C:\\Users\jane\somefile"));
403    }
404}