CVE-2022-31099 (published 2022-06-27T23:15Z)

rulex is a new, portable, regular expression language. When parsing untrusted rulex expressions, the stack may overflow, possibly enabling a Denial of Service attack. This happens when parsing an expression with several hundred levels of nesting, causing the process to abort immediately. This is a security concern for you, if your service parses untrusted rulex expressions (expressions provided by an untrusted user), and your service becomes unavailable when the process running rulex aborts due to a stack overflow. The crash is fixed in version **0.4.3**. Affected users are advised to update to this version. There are no known workarounds for this issue.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:N/A:P)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-674
cwe_name: Uncontrolled Recursion
description: The product does not properly control the amount of recursion that takes place,  consuming excessive resources, such as allocated memory or the program stack.
extended_description: 
        
      
      

Fix information:
repo_url: https://github.com/rulex-rs/rulex
commit_details: repo_url: https://github.com/rulex-rs/rulex
author: Ludwig Stecher
author_date: 2022-06-19 00:16:30+02:00
msg: Add recursion limit
repository: repo_url: https://github.com/rulex-rs/rulex
repo_name: visit repo url
description: visit repo url
repo_language: visit repo url
owner: rulex-rs
file_changes: filename: parse_error.rs
change_type: ModificationType.MODIFY
diff: @@ -95,6 +95,9 @@ pub(crate) enum ParseErrorKind {
     #[error(transparent)]
     Unsupported(UnsupportedError),
 
+    #[error("Recursion limit reached. Try a less nested expression")]
+    RecursionLimit,
+
     #[error("Unknown error: {:?}", .0)]
     Nom(nom::error::ErrorKind),
     #[error("Incomplete parse")]

programming_language: Rust
filename: lib.rs
change_type: ModificationType.MODIFY
diff: @@ -84,7 +84,7 @@ impl<'i> Rulex<'i> {
     /// The parsed `Rulex` can be displayed with `Debug` if the `dbg` feature is
     /// enabled.
     pub fn parse(input: &'i str, options: ParseOptions) -> Result<Self, ParseError> {
-        let rule = parse::parse(input)?;
+        let rule = parse::parse(input, 256)?;
         rule.validate(&options)?;
         Ok(Rulex(rule))
     }

programming_language: Rust
filename: input.rs
change_type: ModificationType.MODIFY
diff: @@ -13,10 +13,15 @@ use super::token::Token;
 pub(crate) struct Input<'i, 'b> {
     source: &'i str,
     tokens: &'b [(Token, Span)],
+    recursion: u16,
 }
 
 impl<'i, 'b> Input<'i, 'b> {
-    pub(super) fn from(source: &'i str, tokens: &'b [(Token, Span)]) -> Result<Self, ParseError> {
+    pub(super) fn from(
+        source: &'i str,
+        tokens: &'b [(Token, Span)],
+        recursion: u16,
+    ) -> Result<Self, ParseError> {
         let error = tokens.iter().find_map(|&(t, span)| match t {
             Token::Error => Some((span, None)),
             Token::ErrorMsg(m) => Some((span, Some(m))),
@@ -29,7 +34,19 @@ impl<'i, 'b> Input<'i, 'b> {
             };
         }
 
-        Ok(Input { source, tokens })
+        Ok(Input { source, tokens, recursion })
+    }
+
+    pub(super) fn recursion_start(&mut self) -> Result<(), ParseError> {
+        self.recursion = self
+            .recursion
+            .checked_sub(1)
+            .ok_or_else(|| ParseErrorKind::RecursionLimit.at(self.span()))?;
+        Ok(())
+    }
+
+    pub(super) fn recursion_end(&mut self) {
+        self.recursion += 1;
     }
 
     pub(super) fn is_empty(&self) -> bool {
@@ -101,7 +118,7 @@ impl<'i, 'b> InputIter for Input<'i, 'b> {
     }
 
     fn iter_elements(&self) -> Self::IterElem {
-        Input { source: self.source, tokens: self.tokens }
+        Input { ..*self }
     }
 
     fn position<P>(&self, predicate: P) -> Option<usize>
@@ -131,12 +148,12 @@ impl<'i, 'b> InputTake for Input<'i, 'b> {
     fn take(&self, count: usize) -> Self {
         let tokens = &self.tokens[..count];
 
-        Input { source: self.source, tokens }
+        Input { tokens, ..*self }
     }
 
     fn take_split(&self, count: usize) -> (Self, Self) {
         let (left, right) = self.tokens.split_at(count);
 
-        (Input { source: self.source, tokens: left }, Input { source: self.source, tokens: right })
+        (Input { tokens: left, ..*self }, Input { tokens: right, ..*self })
     }
 }

programming_language: Rust
filename: parsers.rs
change_type: ModificationType.MODIFY
diff: @@ -35,9 +35,9 @@ use super::{Input, Token};
 
 pub(super) type PResult<'i, 'b, T> = IResult<Input<'i, 'b>, T, ParseError>;
 
-pub(crate) fn parse(source: &str) -> Result<Rule<'_>, ParseError> {
+pub(crate) fn parse(source: &str, recursion: u16) -> Result<Rule<'_>, ParseError> {
     let tokens = super::tokenize::tokenize(source);
-    let input = Input::from(source, &tokens)?;
+    let input = Input::from(source, &tokens, recursion)?;
 
     let (rest, rules) = parse_modified(input)?;
     if rest.is_empty() {
@@ -47,6 +47,22 @@ pub(crate) fn parse(source: &str) -> Result<Rule<'_>, ParseError> {
     }
 }
 
+fn recurse<'i, 'b, O>(
+    mut parser: impl Parser<Input<'i, 'b>, O, ParseError>,
+) -> impl FnMut(Input<'i, 'b>) -> PResult<'i, 'b, O> {
+    move |mut input| {
+        input.recursion_start().map_err(nom::Err::Failure)?;
+
+        match parser.parse(input) {
+            Ok((mut input, output)) => {
+                input.recursion_end();
+                Ok((input, output))
+            }
+            Err(e) => Err(e),
+        }
+    }
+}
+
 pub(super) fn parse_modified<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Rule<'i>> {
     enum ModifierKind {
         Enable,
@@ -78,7 +94,7 @@ pub(super) fn parse_modified<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Ru
                         "let",
                         cut(Token::Identifier),
                         cut(Token::Equals),
-                        cut(parse_or),
+                        cut(recurse(parse_or)),
                         cut(Token::Semicolon),
                     )),
                     |((_, span_start), (name, name_span), _, rule, (_, span_end))| {
@@ -86,7 +102,7 @@ pub(super) fn parse_modified<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Ru
                     },
                 ),
             ))),
-            parse_or,
+            recurse(parse_or),
         ),
         |(stmts, mut rule): (Vec<(Stmt, Span)>, _)| {
             if stmts.len() > 1 {
@@ -137,7 +153,7 @@ pub(super) fn parse_sequence<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Ru
 pub(super) fn parse_fixes<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Rule<'i>> {
     alt((
         try_map(
-            pair(Token::Not, opt(parse_fixes)),
+            pair(Token::Not, opt(recurse(parse_fixes))),
             |(_, rule)| {
                 if let Some(mut rule) = rule {
                     rule.negate()?;
@@ -148,7 +164,7 @@ pub(super) fn parse_fixes<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Rule<
             },
             nom::Err::Failure,
         ),
-        map(pair(parse_lookaround, parse_modified), |((kind, span), rule)| {
+        map(pair(parse_lookaround, recurse(parse_modified)), |((kind, span), rule)| {
             let span = span.join(rule.span());
             Rule::Lookaround(Box::new(Lookaround::new(rule, kind, span)))
         }),
@@ -282,7 +298,10 @@ pub(super) fn parse_group<'i, 'b>(input: Input<'i, 'b>) -> PResult<'i, 'b, Rule<
     }
 
     map(
-        pair(opt(parse_capture), tuple((Token::OpenParen, parse_modified, cut(Token::CloseParen)))),
+        pair(
+            opt(parse_capture),
+            tuple((Token::OpenParen, recurse(parse_modified), cut(Token::CloseParen))),
+        ),
         |(capture, (_, rule, (_, close_paren)))| match (capture, rule) {
             (None, rule) => rule,
             (Some((capture, c_span)), Rule::Group(mut g)) if !g.is_capturing() => {

programming_language: Rust