CVE-2024-26151: Insufficient Information (NVD-CWE-noinfo)
value: The `mjml` PyPI package, found at the `FelixSchwarz/mjml-python` GitHub repo, is an unofficial Python port of MJML, a markup language created by Mailjet. All users of `FelixSchwarz/mjml-python` who insert untrusted data into mjml templates unless that data is checked in a very strict manner. User input like `&lt;script&gt;` would be rendered as `<script>` in the final HTML output. The attacker must be able to control some data which is later injected in an mjml template which is then send out as email to other users. The attacker could control contents of email messages sent through the platform. The problem has been fixed in version 0.11.0 of this library. Versions before 0.10.0 are not affected by this security issue. As a workaround, ensure that potentially untrusted user input does not contain any sequences which could be rendered as HTML.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-02-22T19:15Z
Repository: https://github.com/FelixSchwarz/mjml-python
Language: Python
Files changed: CHANGELOG.md, mjml/elements/head/mj_style.py, mjml/mjml2html.py, tests/testdata/mj-text-escaped-html-expected.html, tests/testdata/mj-text-escaped-html.mjml, tests/upstream_alignment_test.py
Commit date: 2024-02-22 17:46:21 +0100
Commit message: escaped HTML entities like `&gt;` were unescaped in the final mjml output

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index a85de73..7182af3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
 
+0.11.0 (2024-02-xx)
+-------------------
+
+- security fix: escaped HTML entities like `&gt;` were unescaped in the final mjml output, leading to potential injection of untrusted user data (reported by @sh-at-cs)
+
+
 0.10.0 (2023-11-17)
 ------------------
 
 - fix CSS child selectors
 - disable loading remote stylesheets when inlining CSS
diff --git a/mjml/elements/head/mj_style.py b/mjml/elements/head/mj_style.py
index c1e4ed4..1da2024 100644
--- a/mjml/elements/head/mj_style.py
+++ b/mjml/elements/head/mj_style.py
@@ -8,12 +8,17 @@ class MjStyle(HeadComponent):
     component_name = 'mj-style'
 
     @classmethod
     def default_attrs(cls):
         return {
-            'inline'            : '',
+            'inline': '',
         }
 
     def handler(self):
         add = self.context['add']
         inline_attr = 'inlineStyle' if (self.get_attr('inline') == 'inline') else 'style'
-        add(inline_attr, self.getContent())
+        html_str = self.getContent()
+        # CSS can contain child selectors (e.g. "h1 > p") beautifulsoup only
+        # returns escaped entities. To make these selectors work, we need to
+        # unescape these.
+        css_str = html_str.replace('&gt;', '>')
+        add(inline_attr, css_str)
diff --git a/mjml/mjml2html.py b/mjml/mjml2html.py
index 8c542b6..cbef123 100644
--- a/mjml/mjml2html.py
+++ b/mjml/mjml2html.py
@@ -114,11 +114,11 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
             attributes = _mjml.attrs
             children = [child for child in _mjml]
             classes = ignore_empty(attributes.get('mj-class', '').split(' '))
 
             # upstream parses text contents (+ comments) in mjml-parser-xml/index.js
-            content = _mjml.decode_contents(formatter=None)
+            content = _mjml.decode_contents()
 
             attributesClasses = {}
             for css_class in classes:
                 mjClassValues = globalDatas.classes.get(css_class)
                 if mjClassValues:
@@ -211,11 +211,11 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
         for selector, data in globalDatas.htmlAttributes.items():
             for attrName, value in data.items():
                 for element in contentSoup.select(selector):
                     element[attrName] = value or ''
 
-        content = contentSoup.decode_contents(formatter=None)
+        content = contentSoup.decode_contents()
 
     content = skeleton(
         content=content,
         # upstream just passes this extra key to skeleton() as JavaScript
         # won't complain about additional parameters.
diff --git a/tests/testdata/mj-text-escaped-html-expected.html b/tests/testdata/mj-text-escaped-html-expected.html
new file mode 100644
index 0000000..8cbc319
--- /dev/null
+++ b/tests/testdata/mj-text-escaped-html-expected.html
@@ -0,0 +1,120 @@
+<!doctype html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+
+<head>
+  <title>
+  </title>
+  <!--[if !mso]><!-->
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <!--<![endif]-->
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <style type="text/css">
+    #outlook a {
+      padding: 0;
+    }
+
+    body {
+      margin: 0;
+      padding: 0;
+      -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+    }
+
+    table,
+    td {
+      border-collapse: collapse;
+      mso-table-lspace: 0pt;
+      mso-table-rspace: 0pt;
+    }
+
+    img {
+      border: 0;
+      height: auto;
+      line-height: 100%;
+      outline: none;
+      text-decoration: none;
+      -ms-interpolation-mode: bicubic;
+    }
+
+    p {
+      display: block;
+      margin: 13px 0;
+    }
+
+  </style>
+  <!--[if mso]>
+    <noscript>
+    <xml>
+    <o:OfficeDocumentSettings>
+      <o:AllowPNG/>
+      <o:PixelsPerInch>96</o:PixelsPerInch>
+    </o:OfficeDocumentSettings>
+    </xml>
+    </noscript>
+    <![endif]-->
+  <!--[if lte mso 11]>
+    <style type="text/css">
+      .mj-outlook-group-fix { width:100% !important; }
+    </style>
+    <![endif]-->
+  <!--[if !mso]><!-->
+  <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
+  <style type="text/css">
+    @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
+
+  </style>
+  <!--<![endif]-->
+  <style type="text/css">
+    @media only screen and (min-width:480px) {
+      .mj-column-per-100 {
+        width: 100% !important;
+        max-width: 100%;
+      }
+    }
+
+  </style>
+  <style media="screen and (min-width:480px)">
+    .moz-text-html .mj-column-per-100 {
+      width: 100% !important;
+      max-width: 100%;
+    }
+
+  </style>
+  <style type="text/css">
+  </style>
+  <style type="text/css">
+  </style>
+</head>
+
+<body style="word-spacing:normal;">
+  <div style="">
+    <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
+    <div style="margin:0px auto;max-width:600px;">
+      <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
+        <tbody>
+          <tr>
+            <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
+              <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
+              <div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
+                <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
+                  <tbody>
+                    <tr>
+                      <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
+                        <div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">Pretty unsafe: &lt;script&gt;</div>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+              <!--[if mso | IE]></td></tr></table><![endif]-->
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    <!--[if mso | IE]></td></tr></table><![endif]-->
+  </div>
+</body>
+
+</html>
diff --git a/tests/testdata/mj-text-escaped-html.mjml b/tests/testdata/mj-text-escaped-html.mjml
new file mode 100644
index 0000000..fee2754
--- /dev/null
+++ b/tests/testdata/mj-text-escaped-html.mjml
@@ -0,0 +1,11 @@
+<mjml>
+  <mj-body>
+  <mj-section>
+    <mj-column>
+    <mj-text>
+      Pretty unsafe: &lt;script&gt;
+    </mj-text>
+    </mj-column>
+    </mj-section>
+  </mj-body>
+</mjml>
diff --git a/tests/upstream_alignment_test.py b/tests/upstream_alignment_test.py
index cb64d5b..162f8ab 100644
--- a/tests/upstream_alignment_test.py
+++ b/tests/upstream_alignment_test.py
@@ -46,10 +46,11 @@ TEST_IDS = (
     'mj-raw-with-tags',
     'mj-raw-head',
     'mj-raw-head-with-tags',
     'mj-social',
     'mj-spacer',
+    'mj-text-escaped-html', # this test is security-critical
     'mj-wrapper',
 )
 
 @pytest.mark.parametrize('test_id', TEST_IDS)
 def test_ensure_same_html_as_upstream(test_id):

Vulnerable code:             'inline'            : '',
        add(inline_attr, self.getContent())
            content = _mjml.decode_contents(formatter=None)
        content = contentSoup.decode_contents(formatter=None)
Fixed code: 0.11.0 (2024-02-xx)
-------------------

- security fix: escaped HTML entities like `&gt;` were unescaped in the final mjml output, leading to potential injection of untrusted user data (reported by @sh-at-cs)


            'inline': '',
        html_str = self.getContent()
        # CSS can contain child selectors (e.g. "h1 > p") beautifulsoup only
        # returns escaped entities. To make these selectors work, we need to
        # unescape these.
        css_str = html_str.replace('&gt;', '>')
        add(inline_attr, css_str)
            content = _mjml.decode_contents()
        content = contentSoup.decode_contents()
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">

<head>
  <title>
  </title>
  <!--[if !mso]><!-->
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <!--<![endif]-->
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    #outlook a {
      padding: 0;
    }

    body {
      margin: 0;
      padding: 0;
      -webkit-text-size-adjust: 100%;
      -ms-text-size-adjust: 100%;
    }

    table,
    td {
      border-collapse: collapse;
      mso-table-lspace: 0pt;
      mso-table-rspace: 0pt;
    }

    img {
      border: 0;
      height: auto;
      line-height: 100%;
      outline: none;
      text-decoration: none;
      -ms-interpolation-mode: bicubic;
    }

    p {
      display: block;
      margin: 13px 0;
    }

  </style>
  <!--[if mso]>
    <noscript>
    <xml>
    <o:OfficeDocumentSettings>
      <o:AllowPNG/>
      <o:PixelsPerInch>96</o:PixelsPerInch>
    </o:OfficeDocumentSettings>
    </xml>
    </noscript>
    <![endif]-->
  <!--[if lte mso 11]>
    <style type="text/css">
      .mj-outlook-group-fix { width:100% !important; }
    </style>
    <![endif]-->
  <!--[if !mso]><!-->
  <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
  <style type="text/css">
    @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);

  </style>
  <!--<![endif]-->
  <style type="text/css">
    @media only screen and (min-width:480px) {
      .mj-column-per-100 {
        width: 100% !important;
        max-width: 100%;
      }
    }

  </style>
  <style media="screen and (min-width:480px)">
    .moz-text-html .mj-column-per-100 {
      width: 100% !important;
      max-width: 100%;
    }

  </style>
  <style type="text/css">
  </style>
  <style type="text/css">
  </style>
</head>

<body style="word-spacing:normal;">
  <div style="">
    <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
    <div style="margin:0px auto;max-width:600px;">
      <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
        <tbody>
          <tr>
            <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
              <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
              <div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
                <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
                  <tbody>
                    <tr>
                      <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
                        <div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">Pretty unsafe: &lt;script&gt;</div>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </div>
              <!--[if mso | IE]></td></tr></table><![endif]-->
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <!--[if mso | IE]></td></tr></table><![endif]-->
  </div>
</body>

</html>
<mjml>
  <mj-body>
  <mj-section>
    <mj-column>
    <mj-text>
      Pretty unsafe: &lt;script&gt;
    </mj-text>
    </mj-column>
    </mj-section>
  </mj-body>
</mjml>
    'mj-text-escaped-html', # this test is security-critical