VYPR
Medium severity5.3NVD Advisory· Published Jul 25, 2022· Updated Aug 3, 2024

Cross-site Scripting (XSS) - Reflected in beancount/fava

CVE-2022-2514

Description

The time and filter parameters in Fava prior to v1.22 are vulnerable to reflected XSS due to the lack of escaping of error messages which contained the parameters in verbatim.

AI Insight

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

Fava prior to v1.22 is vulnerable to reflected XSS due to unsanitized error messages containing the `time` and `filter` parameters.

CVE-2022-2514 is a reflected cross-site scripting (XSS) vulnerability in Fava, an open-source web interface for the Beancount double-entry accounting tool. The flaw affects versions prior to v1.22. The root cause is that error messages generated from the time and filter parameters were returned verbatim without proper HTML escaping, allowing an attacker to inject arbitrary JavaScript code through these parameters [1][3].

The vulnerability is exploitable by crafting a malicious URL containing a payload in either the time or filter query parameters. Since the vulnerability is reflected, no authentication is strictly required to trigger it if the attacker can convince a target user to visit the crafted link. The attack vector is network-based, requires low attack complexity, and does not demand any special privileges or user interaction beyond clicking the link [1][3].

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's session within the Fava application. This could lead to theft of session cookies, unauthorized actions performed on behalf of the authenticated user, or defacement of the application interface. The full impact aligns with typical reflected XSS consequences, as outlined in the advisory [1][3].

The issue has been patched in Fava version 1.22. The fix, introduced in commit ca9e3882c7b5fbf5273ba52340b9fea6a99f3711, addresses the problem by wrapping rendered HTML strings with Markup() from the markupsafe library instead of using the |safe template filter, which improperly marked content as safe without escaping [2]. Users are strongly advised to upgrade to v1.22 or later. Mitigations may include applying input validation and output encoding on the server side if an immediate update is not possible [4].

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 products

2
  • beancount/favallm-fuzzy
    Range: <1.22
  • beancount/beancount/favav5
    Range: unspecified

Patches

1
ca9e3882c7b5

avoid use of |safe filter in templates

https://github.com/beancount/favaJakob SchnitzerJun 30, 2022via ghsa
6 files changed · +27 18
  • src/fava/application.py+7 4 modified
    @@ -35,6 +35,7 @@
     from flask.wrappers import Response
     from flask_babel import Babel  # type: ignore
     from flask_babel import get_translations
    +from markupsafe import Markup
     from werkzeug.utils import secure_filename
     
     from fava import __version__ as fava_version
    @@ -384,10 +385,12 @@ def help_page(page_slug: str) -> str:
             "_layout.html",
             active_page="help",
             page_slug=page_slug,
    -        help_html=render_template_string(
    -            html,
    -            beancount_version=beancount_version,
    -            fava_version=fava_version,
    +        help_html=Markup(
    +            render_template_string(
    +                html,
    +                beancount_version=beancount_version,
    +                fava_version=fava_version,
    +            )
             ),
             HELP_PAGES=HELP_PAGES,
         )
    
  • src/fava/core/file.py+11 6 modified
    @@ -22,6 +22,7 @@
     from beancount.core.flags import FLAG_SUMMARIZE
     from beancount.core.flags import FLAG_TRANSFER
     from beancount.parser.printer import format_entry  # type: ignore
    +from markupsafe import Markup
     
     from fava.core._compat import FLAG_RETURNS
     from fava.core._compat import FLAG_UNREALIZED
    @@ -176,7 +177,9 @@ def insert_entries(self, entries: Entries) -> None:
                     )
                     self.ledger.extensions.after_insert_entry(entry)
     
    -    def render_entries(self, entries: Entries) -> Generator[str, None, None]:
    +    def render_entries(
    +        self, entries: Entries
    +    ) -> Generator[Markup, None, None]:
             """Return entries in Beancount format.
     
             Only renders :class:`.Balance` and :class:`.Transaction`.
    @@ -193,12 +196,14 @@ def render_entries(self, entries: Entries) -> Generator[str, None, None]:
                     if isinstance(entry, Transaction) and entry.flag in EXCL_FLAGS:
                         continue
                     try:
    -                    yield get_entry_slice(entry)[0] + "\n"
    +                    yield Markup(get_entry_slice(entry)[0] + "\n")
                     except (KeyError, FileNotFoundError):
    -                    yield _format_entry(
    -                        entry,
    -                        self.ledger.fava_options.currency_column,
    -                        indent,
    +                    yield Markup(
    +                        _format_entry(
    +                            entry,
    +                            self.ledger.fava_options.currency_column,
    +                            indent,
    +                        )
                         )
     
     
    
  • src/fava/template_filters.py+6 5 modified
    @@ -12,14 +12,15 @@
     from typing import MutableMapping
     from typing import TypeVar
     
    -import flask
     from beancount.core import compare
     from beancount.core import realization
     from beancount.core.account import ACCOUNT_RE
     from beancount.core.data import Directive
     from beancount.core.inventory import Inventory
     from beancount.core.number import Decimal
     from beancount.core.number import ZERO
    +from flask import url_for
    +from markupsafe import Markup
     
     from fava.context import g
     from fava.core.conversion import cost
    @@ -145,14 +146,14 @@ def basename(file_path: str) -> str:
         return unicodedata.normalize("NFC", os.path.basename(file_path))
     
     
    -def format_errormsg(message: str) -> str:
    +def format_errormsg(message: str) -> Markup:
         """Match account names in error messages and insert HTML links for them."""
         match = re.search(ACCOUNT_RE, message)
         if not match:
    -        return message
    +        return Markup(message)
         account = match.group()
    -    url = flask.url_for("account", name=account)
    -    return (
    +    url = url_for("account", name=account)
    +    return Markup(
             message.replace(account, f'<a href="{url}">{account}</a>')
             .replace("for '", "for ")
             .replace("': ", ": ")
    
  • src/fava/templates/errors.html+1 1 modified
    @@ -13,7 +13,7 @@
           {% with link=url_for_source(file_path=error.source['filename'], line=error.source['lineno']) %}
           <td><a class="source" href="{{ link }}" title="{{ _('Show source %(file)s:%(lineno)s', file=error.source['filename'], lineno=error.source['lineno']) }}">{{ error.source['filename'] }}</a></td>
           <td class="num"><a class="source" href="{{ link }}" title="{{ _('Show source %(file)s:%(lineno)s', file=error.source['filename'], lineno=error.source['lineno']) }}">{{ error.source['lineno'] }}</a></td>
    -      <td class="pre">{{ error.message|format_errormsg|safe }}</td>
    +      <td class="pre">{{ error.message|format_errormsg }}</td>
           {% endwith %}
         </tr>
         {% endfor %}
    
  • src/fava/templates/help.html+1 1 modified
    @@ -12,6 +12,6 @@ <h3>{{ _('Help pages') }}</h3>
         </ul>
       </div>
       <div class="help-text">
    -    {{ help_html|safe }}
    +    {{ help_html }}
       </div>
     </div>
    
  • src/fava/templates/_layout.html+1 1 modified
    @@ -43,7 +43,7 @@ <h1>
           <svelte-component type="charts"></svelte-component>
           {% block content %}
           {% if content %}
    -      {{ content|safe }}
    +      {{ content }}
           {% else %}
           {% include active_page + '.html' %}
           {% endif %}
    

Vulnerability mechanics

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

References

6

News mentions

0

No linked articles in our index yet.