VYPR
Moderate severityNVD Advisory· Published Mar 26, 2021· Updated Aug 3, 2024

HTML injection in email and account expiry notifications

CVE-2021-21333

Description

Synapse is a Matrix reference homeserver written in python (pypi package matrix-synapse). Matrix is an ecosystem for open federated Instant Messaging and VoIP. In Synapse before version 1.27.0, the notification emails sent for notifications for missed messages or for an expiring account are subject to HTML injection. In the case of the notification for missed messages, this could allow an attacker to insert forged content into the email. The account expiry feature is not enabled by default and the HTML injection is not controllable by an attacker. This is fixed in version 1.27.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An HTML injection vulnerability in Synapse before 1.27.0 allowed attackers to insert forged content into notification emails for missed messages.

Vulnerability

Description

CVE-2021-21333 is an HTML injection vulnerability in Synapse, the reference Matrix homeserver written in Python. The vulnerability exists in the notification emails sent for missed messages or expiring accounts. The root cause is that email templates were not properly sanitized before rendering, allowing an attacker to inject arbitrary HTML content into the email body. This issue affected versions prior to 1.27.0 [1][2].

Exploitation

Prerequisites

Exploitation requires the attacker to be able to trigger notification emails, such as by sending a message that generates a missed-message notification. The account expiry feature, which is not enabled by default, also had this vulnerability but is not controllable by an attacker. The injection occurs when the email client renders the HTML, potentially allowing the attacker to craft emails that appear legitimate or contain forged content [1][2].

Impact

A successful exploit could allow an attacker to insert forged or malicious content into the email, potentially leading to phishing attacks or confusion. However, the vulnerability does not directly lead to server compromise or data exfiltration, as the injection is limited to the email content itself. The CVSS score and vector are not provided by NVD [2].

Mitigation

The vulnerability is fixed in Synapse version 1.27.0, released on 2021-02-25. The fix involved enabling Jinja2's autoescape for HTML templates, which prevents HTML injection by escaping special characters. Upgrading to 1.27.0 or later is recommended. The patch also cleaned up template loading code to ensure consistent autoescaping [1][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
matrix-synapsePyPI
< 1.27.01.27.0

Affected products

2

Patches

1
e54746bdf7d5

Clean-up the template loading code. (#9200)

https://github.com/matrix-org/synapsePatrick ClokeJan 27, 2021via ghsa
12 files changed · +96 38
  • changelog.d/9200.misc+1 0 added
    @@ -0,0 +1 @@
    +Clean-up template loading code.
    
  • synapse/config/_base.py+26 16 modified
    @@ -203,36 +203,50 @@ def read_file(cls, file_path, config_name):
             with open(file_path) as file_stream:
                 return file_stream.read()
     
    +    def read_template(self, filename: str) -> jinja2.Template:
    +        """Load a template file from disk.
    +
    +        This function will attempt to load the given template from the default Synapse
    +        template directory.
    +
    +        Files read are treated as Jinja templates. The templates is not rendered yet
    +        and has autoescape enabled.
    +
    +        Args:
    +            filename: A template filename to read.
    +
    +        Raises:
    +            ConfigError: if the file's path is incorrect or otherwise cannot be read.
    +
    +        Returns:
    +            A jinja2 template.
    +        """
    +        return self.read_templates([filename])[0]
    +
         def read_templates(
    -        self,
    -        filenames: List[str],
    -        custom_template_directory: Optional[str] = None,
    -        autoescape: bool = False,
    +        self, filenames: List[str], custom_template_directory: Optional[str] = None,
         ) -> List[jinja2.Template]:
             """Load a list of template files from disk using the given variables.
     
             This function will attempt to load the given templates from the default Synapse
             template directory. If `custom_template_directory` is supplied, that directory
             is tried first.
     
    -        Files read are treated as Jinja templates. These templates are not rendered yet.
    +        Files read are treated as Jinja templates. The templates are not rendered yet
    +        and have autoescape enabled.
     
             Args:
                 filenames: A list of template filenames to read.
     
                 custom_template_directory: A directory to try to look for the templates
                     before using the default Synapse template directory instead.
     
    -            autoescape: Whether to autoescape variables before inserting them into the
    -                template.
    -
             Raises:
                 ConfigError: if the file's path is incorrect or otherwise cannot be read.
     
             Returns:
                 A list of jinja2 templates.
             """
    -        templates = []
             search_directories = [self.default_template_dir]
     
             # The loader will first look in the custom template directory (if specified) for the
    @@ -249,7 +263,7 @@ def read_templates(
                 search_directories.insert(0, custom_template_directory)
     
             loader = jinja2.FileSystemLoader(search_directories)
    -        env = jinja2.Environment(loader=loader, autoescape=autoescape)
    +        env = jinja2.Environment(loader=loader, autoescape=jinja2.select_autoescape(),)
     
             # Update the environment with our custom filters
             env.filters.update(
    @@ -259,12 +273,8 @@ def read_templates(
                 }
             )
     
    -        for filename in filenames:
    -            # Load the template
    -            template = env.get_template(filename)
    -            templates.append(template)
    -
    -        return templates
    +        # Load the templates
    +        return [env.get_template(filename) for filename in filenames]
     
     
     def _format_ts_filter(value: int, format: str):
    
  • synapse/config/captcha.py+1 3 modified
    @@ -28,9 +28,7 @@ def read_config(self, config, **kwargs):
                 "recaptcha_siteverify_api",
                 "https://www.recaptcha.net/recaptcha/api/siteverify",
             )
    -        self.recaptcha_template = self.read_templates(
    -            ["recaptcha.html"], autoescape=True
    -        )[0]
    +        self.recaptcha_template = self.read_template("recaptcha.html")
     
         def generate_config_section(self, **kwargs):
             return """\
    
  • synapse/config/consent_config.py+1 1 modified
    @@ -89,7 +89,7 @@ def __init__(self, *args):
     
         def read_config(self, config, **kwargs):
             consent_config = config.get("user_consent")
    -        self.terms_template = self.read_templates(["terms.html"], autoescape=True)[0]
    +        self.terms_template = self.read_template("terms.html")
     
             if consent_config is None:
                 return
    
  • synapse/config/registration.py+1 3 modified
    @@ -176,9 +176,7 @@ def read_config(self, config, **kwargs):
             self.session_lifetime = session_lifetime
     
             # The success template used during fallback auth.
    -        self.fallback_success_template = self.read_templates(
    -            ["auth_success.html"], autoescape=True
    -        )[0]
    +        self.fallback_success_template = self.read_template("auth_success.html")
     
         def generate_config_section(self, generate_secrets=False, **kwargs):
             if generate_secrets:
    
  • synapse/push/mailer.py+16 2 modified
    @@ -668,6 +668,15 @@ def make_unsubscribe_link(
     
     
     def safe_markup(raw_html: str) -> jinja2.Markup:
    +    """
    +    Sanitise a raw HTML string to a set of allowed tags and attributes, and linkify any bare URLs.
    +
    +    Args
    +        raw_html: Unsafe HTML.
    +
    +    Returns:
    +        A Markup object ready to safely use in a Jinja template.
    +    """
         return jinja2.Markup(
             bleach.linkify(
                 bleach.clean(
    @@ -684,8 +693,13 @@ def safe_markup(raw_html: str) -> jinja2.Markup:
     
     def safe_text(raw_text: str) -> jinja2.Markup:
         """
    -    Process text: treat it as HTML but escape any tags (ie. just escape the
    -    HTML) then linkify it.
    +    Sanitise text (escape any HTML tags), and then linkify any bare URLs.
    +
    +    Args
    +        raw_text: Unsafe text which might include HTML markup.
    +
    +    Returns:
    +        A Markup object ready to safely use in a Jinja template.
         """
         return jinja2.Markup(
             bleach.linkify(bleach.clean(raw_text, tags=[], attributes={}, strip=False))
    
  • synapse/res/templates/sso_auth_bad_user.html+1 1 modified
    @@ -5,7 +5,7 @@
         <body>
             <div>
                 <p>
    -                We were unable to validate your <tt>{{server_name | e}}</tt> account via
    +                We were unable to validate your <tt>{{ server_name }}</tt> account via
                     single-sign-on (SSO), because the SSO Identity Provider returned
                     different details than when you logged in.
                 </p>
    
  • synapse/res/templates/sso_auth_confirm.html+2 2 modified
    @@ -5,8 +5,8 @@
         <body>
             <div>
                 <p>
    -                A client is trying to {{ description | e }}. To confirm this action,
    -                <a href="{{ redirect_url | e }}">re-authenticate with single sign-on</a>.
    +                A client is trying to {{ description }}. To confirm this action,
    +                <a href="{{ redirect_url }}">re-authenticate with single sign-on</a>.
                     If you did not expect this, your account may be compromised!
                 </p>
             </div>
    
  • synapse/res/templates/sso_error.html+1 1 modified
    @@ -12,7 +12,7 @@
         <p>
             There was an error during authentication:
         </p>
    -    <div id="errormsg" style="margin:20px 80px">{{ error_description | e }}</div>
    +    <div id="errormsg" style="margin:20px 80px">{{ error_description }}</div>
         <p>
             If you are seeing this page after clicking a link sent to you via email, make
             sure you only click the confirmation link once, and that you open the
    
  • synapse/res/templates/sso_login_idp_picker.html+6 6 modified
    @@ -3,22 +3,22 @@
         <head>
             <meta charset="UTF-8">
             <link rel="stylesheet" href="/_matrix/static/client/login/style.css">
    -        <title>{{server_name | e}} Login</title>
    +        <title>{{ server_name }} Login</title>
         </head>
         <body>
             <div id="container">
    -            <h1 id="title">{{server_name | e}} Login</h1>
    +            <h1 id="title">{{ server_name }} Login</h1>
                 <div class="login_flow">
                     <p>Choose one of the following identity providers:</p>
                 <form>
    -                <input type="hidden" name="redirectUrl" value="{{redirect_url | e}}">
    +                <input type="hidden" name="redirectUrl" value="{{ redirect_url }}">
                     <ul class="radiobuttons">
     {% for p in providers %}
                         <li>
    -                        <input type="radio" name="idp" id="prov{{loop.index}}" value="{{p.idp_id}}">
    -                        <label for="prov{{loop.index}}">{{p.idp_name | e}}</label>
    +                        <input type="radio" name="idp" id="prov{{ loop.index }}" value="{{ p.idp_id }}">
    +                        <label for="prov{{ loop.index }}">{{ p.idp_name }}</label>
     {% if p.idp_icon %}
    -                        <img src="{{p.idp_icon | mxc_to_http(32, 32)}}"/>
    +                        <img src="{{ p.idp_icon | mxc_to_http(32, 32) }}"/>
     {% endif %}
                         </li>
     {% endfor %}
    
  • synapse/res/templates/sso_redirect_confirm.html+3 3 modified
    @@ -5,10 +5,10 @@
         <title>SSO redirect confirmation</title>
     </head>
         <body>
    -        <p>The application at <span style="font-weight:bold">{{ display_url | e }}</span> is requesting full access to your <span style="font-weight:bold">{{ server_name }}</span> Matrix account.</p>
    +        <p>The application at <span style="font-weight:bold">{{ display_url }}</span> is requesting full access to your <span style="font-weight:bold">{{ server_name }}</span> Matrix account.</p>
             <p>If you don't recognise this address, you should ignore this and close this tab.</p>
             <p>
    -            <a href="{{ redirect_url | e }}">I trust this address</a>
    +            <a href="{{ redirect_url }}">I trust this address</a>
             </p>
         </body>
    -</html>
    \ No newline at end of file
    +</html>
    
  • UPGRADE.rst+37 0 modified
    @@ -85,6 +85,43 @@ for example:
          wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
          dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
     
    +Upgrading to v1.27.0
    +====================
    +
    +Changes to HTML templates
    +-------------------------
    +
    +The HTML templates for SSO and email notifications now have `Jinja2's autoescape <https://jinja.palletsprojects.com/en/2.11.x/api/#autoescaping>`_
    +enabled for files ending in ``.html``, ``.htm``, and ``.xml``. If you hae customised
    +these templates and see issues when viewing them you might need to update them.
    +It is expected that most configurations will need no changes.
    +
    +If you have customised the templates *names* for these templates it is recommended
    +to verify they end in ``.html`` to ensure autoescape is enabled.
    +
    +The above applies to the following templates:
    +
    +* ``add_threepid.html``
    +* ``add_threepid_failure.html``
    +* ``add_threepid_success.html``
    +* ``notice_expiry.html``
    +* ``notice_expiry.html``
    +* ``notif_mail.html`` (which, by default, includes ``room.html`` and ``notif.html``)
    +* ``password_reset.html``
    +* ``password_reset_confirmation.html``
    +* ``password_reset_failure.html``
    +* ``password_reset_success.html``
    +* ``registration.html``
    +* ``registration_failure.html``
    +* ``registration_success.html``
    +* ``sso_account_deactivated.html``
    +* ``sso_auth_bad_user.html``
    +* ``sso_auth_confirm.html``
    +* ``sso_auth_success.html``
    +* ``sso_error.html``
    +* ``sso_login_idp_picker.html``
    +* ``sso_redirect_confirm.html``
    +
     Upgrading to v1.26.0
     ====================
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.