CVE-2024-26151 (published 2024-02-22T19:15Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/FelixSchwarz/mjml-python
commit_details: repo_url: https://github.com/FelixSchwarz/mjml-python
author: Casey Holzer
author_date: 2023-09-28 14:08:09-06:00
msg: Fix CSS child selectors; don't load remote stylesheets
repository: repo_url: https://github.com/FelixSchwarz/mjml-python
repo_name: FelixSchwarz/mjml-python
description: Python implementation for MJML - a framework that makes responsive-email easy
repo_language: Python
owner: FelixSchwarz
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,8 @@
 0.10.0 (?)
 ------------------
 
+- fix CSS child selectors
+- disable loading remote stylesheets
 - require css-inline 0.11.x for performance improvements
 - drop css-inline Python 3.6 support
 - fix exception when processing an `mj-section` with `background-size` (reported by Thomas Handorf)

programming_language: Markdown
filename: mjml2html.py
change_type: ModificationType.MODIFY
diff: @@ -115,7 +115,7 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
             classes = ignore_empty(attributes.get('mj-class', '').split(' '))
 
             # upstream parses text contents (+ comments) in mjml-parser-xml/index.js
-            content = _mjml.decode_contents()
+            content = _mjml.decode_contents(formatter=None)
 
             attributesClasses = {}
             for css_class in classes:
@@ -212,7 +212,7 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
                 for element in contentSoup.select(selector):
                     element[attrName] = value or ''
 
-        content = contentSoup.decode_contents()
+        content = contentSoup.decode_contents(formatter=None)
 
     content = skeleton(
         content=content,
@@ -235,6 +235,7 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
             inline_style_tags=False,
             keep_link_tags=True,
             keep_style_tags=True,
+            load_remote_stylesheets=False,
         )
         content = inliner.inline(content)
 

programming_language: Python
filename: css-inlining-expected.html
change_type: ModificationType.MODIFY
diff: @@ -124,7 +124,7 @@
                     </tr>
                     <tr>
                       <td align="left" class="box" style="border: 5px solid red; font-size: 0px; padding: 10px 25px; word-break: break-word;">
-                        <div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#F45E43;">Hello World</div>
+                        <div style="font-family:helvetica;font-size:20px;line-height:1;text-align:left;color:#F45E43;"><span style="border: 3px solid blue;"> &lt; Hello World &gt; </span></div>
                       </td>
                     </tr>
                   </tbody>

programming_language: HTML
filename: css-inlining.mjml
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,9 @@
       .box {
         border: 5px solid red;
       }
+      .box > div > span {
+        border: 3px solid blue;
+      }
     </mj-style>
   </mj-head>
   <mj-body>
@@ -14,7 +17,11 @@
 
         <mj-divider border-color="#F45E43"></mj-divider>
 
-        <mj-text css-class="box" font-size="20px" color="#F45E43" font-family="helvetica">Hello World</mj-text>
+        <mj-text css-class="box" font-size="20px" color="#F45E43" font-family="helvetica">
+          <span>
+            &lt; Hello World &gt;
+          </span>
+        </mj-text>
 
       </mj-column>
     </mj-section>

programming_language: HTML
repo_url: https://github.com/FelixSchwarz/mjml-python
commit_details: repo_url: https://github.com/FelixSchwarz/mjml-python
author: Felix Schwarz
author_date: 2024-02-20 22:18:35+01:00
msg: escaped HTML entities like `&gt;` were unescaped in the final mjml output
repository: repo_url: https://github.com/FelixSchwarz/mjml-python
repo_name: FelixSchwarz/mjml-python
description: Python implementation for MJML - a framework that makes responsive-email easy
repo_language: Python
owner: FelixSchwarz
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,10 @@
 
+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)
 ------------------
 

programming_language: Markdown
filename: mj_style.py
change_type: ModificationType.MODIFY
diff: @@ -10,10 +10,15 @@ class MjStyle(HeadComponent):
     @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)

programming_language: Python
filename: mjml2html.py
change_type: ModificationType.MODIFY
diff: @@ -116,7 +116,7 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
             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:
@@ -213,7 +213,7 @@ def mjml_to_html(xml_fp_or_json, skeleton=None, template_dir=None,
                 for element in contentSoup.select(selector):
                     element[attrName] = value or ''
 
-        content = contentSoup.decode_contents(formatter=None)
+        content = contentSoup.decode_contents()
 
     content = skeleton(
         content=content,

programming_language: Python
filename: mj-text-escaped-html-expected.html
change_type: ModificationType.ADD
diff: @@ -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>

programming_language: HTML
filename: mj-text-escaped-html.mjml
change_type: ModificationType.ADD
diff: @@ -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>

programming_language: HTML
filename: upstream_alignment_test.py
change_type: ModificationType.MODIFY
diff: @@ -48,6 +48,7 @@ TEST_IDS = (
     'mj-raw-head-with-tags',
     'mj-social',
     'mj-spacer',
+    'mj-text-escaped-html', # this test is security-critical
     'mj-wrapper',
 )
 

programming_language: Python