Vulnerability: Regular Expression Denial of Service in CairoSVG # Doyensec Vulnerability Advisory 

* Regular Expression Denial of Service (REDoS) in cairosvg
* Affected Product: CairoSVG v2.0.0+
* Vendor: https://github.com/Kozea
* Severity: Medium
* Vulnerability Class: Denial of Service
* Author(s): Ben Caller ([Doyensec](https://doyensec.com))

## Summary

When processing SVG files, the python package CairoSVG uses two regular expressions which are vulnerable to Regular Expression Denial of Service (REDoS).
If an attacker provides a malicious SVG, it can make cairosvg get stuck processing the file for a very long time.

## Technical description

The vulnerable regular expressions are

https://github.com/Kozea/CairoSVG/blob/9c4a982b9a021280ad90e89707eacc1d114e4ac4/cairosvg/colors.py#L190-L191

The section between 'rgb(' and the final ')' contains multiple overlapping groups.

Since all three infinitely repeating groups accept spaces, a long string of spaces causes catastrophic backtracking when it is not followed by a closing parenthesis.

The complexity is cubic, so doubling the length of the malicious string of spaces makes processing take 8 times as long.

## Reproduction steps

Create a malicious SVG of the form:

    <svg width="1" height="1"><rect fill="rgb(                     ;"/></svg>

with the following code:

    '<svg width="1" height="1"><rect fill="rgb(' + (' ' * 3456) + ';"/></svg>'

Note that there is no closing parenthesis before the semi-colon.

Run cairosvg e.g.:

    cairosvg cairo-redos.svg -o x.png

and notice that it hangs at 100% CPU. Increasing the number of spaces increases the processing time with cubic complexity.

## Remediation

Fix the regexes to avoid overlapping parts. Perhaps remove the [ \n\r\t]* groups from the regex, and use .strip() on the returned capture group.

## Disclosure timeline

- 2020-12-30: Vulnerability disclosed via email to CourtBouillon
Vulnerability ID: GHSA-hq37-853p-g5cf
CWE: CWE-400
Severity score: 0
Project: https://github.com/Kozea/CairoSVG
Published: 2021-01-06
File: cairosvg/colors.py
Commit message: Merge pull request from GHSA-hq37-853p-g5cf

Don’t use overlapping groups for regular expressions
Commit author: Guillaume Ayoub

Vulnerable version (before fix):
     'transparent': (0, 0, 0, 0), }  RGBA = re.compile(r'rgba\([ \n\r\t]*(.+?)[ \n\r\t]*\)') RGB = re.compile(r'rgb\([ \n\r\t]*(.+?)[ \n\r\t]*\)') HEX_RRGGBB = re.compile('#[0-9a-f]{6}') HEX_RGB = re.compile('#[0-9a-f]{3}')  def color(string, opacity=1):     if match:         r, g, b, a = tuple(             float(i.strip(' %')) / 100 if '%' in i else float(i) / 255             for i in match.group(1).split(','))         return (r, g, b, a * 255 * opacity)      match = RGB.search(string)     if match:         r, g, b = tuple(             float(i.strip(' %')) / 100 if '%' in i else float(i) / 255             for i in match.group(1).split(','))         return (r, g, b, opacity)      match = HEX_RRGGBB.search(string)

Fixed version (after fix):
     'transparent': (0, 0, 0, 0), }  RGBA = re.compile(r'rgba\((.+?)\)') RGB = re.compile(r'rgb\((.+?)\)') HEX_RRGGBB = re.compile('#[0-9a-f]{6}') HEX_RGB = re.compile('#[0-9a-f]{3}')  def color(string, opacity=1):     if match:         r, g, b, a = tuple(             float(i.strip(' %')) / 100 if '%' in i else float(i) / 255             for i in match.group(1).strip().split(','))         return (r, g, b, a * 255 * opacity)      match = RGB.search(string)     if match:         r, g, b = tuple(             float(i.strip(' %')) / 100 if '%' in i else float(i) / 255             for i in match.group(1).strip().split(','))         return (r, g, b, opacity)      match = HEX_RRGGBB.search(string)

Patch:
patch: @@ -187,8 +187,8 @@
     'transparent': (0, 0, 0, 0),
 }
 
-RGBA = re.compile(r'rgba\([ \n\r\t]*(.+?)[ \n\r\t]*\)')
-RGB = re.compile(r'rgb\([ \n\r\t]*(.+?)[ \n\r\t]*\)')
+RGBA = re.compile(r'rgba\((.+?)\)')
+RGB = re.compile(r'rgb\((.+?)\)')
 HEX_RRGGBB = re.compile('#[0-9a-f]{6}')
 HEX_RGB = re.compile('#[0-9a-f]{3}')
 
@@ -212,14 +212,14 @@ def color(string, opacity=1):
     if match:
         r, g, b, a = tuple(
             float(i.strip(' %')) / 100 if '%' in i else float(i) / 255
-            for i in match.group(1).split(','))
+            for i in match.group(1).strip().split(','))
         return (r, g, b, a * 255 * opacity)
 
     match = RGB.search(string)
     if match:
         r, g, b = tuple(
             float(i.strip(' %')) / 100 if '%' in i else float(i) / 255
-            for i in match.group(1).split(','))
+            for i in match.group(1).strip().split(','))
         return (r, g, b, opacity)
 
     match = HEX_RRGGBB.search(string)
path: cairosvg/colors.py