Cross-site Scripting (XSS) - Reflected in beancount/fava
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/beancount/favav5Range: unspecified
Patches
1ca9e3882c7b5avoid use of |safe filter in templates
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- github.com/advisories/GHSA-xrf4-39fm-j5f2ghsaADVISORY
- github.com/beancount/fava/commit/ca9e3882c7b5fbf5273ba52340b9fea6a99f3711ghsax_refsource_MISC
- huntr.dev/bounties/dbf77139-4384-4dc5-9994-45a5e0747429ghsax_refsource_CONFIRM
- github.com/pypa/advisory-database/tree/main/vulns/fava/PYSEC-2022-239.yamlghsa
- github.com/pypa/advisory-database/tree/main/vulns/fava/PYSEC-2022-43182.yamlghsa
- nvd.nist.gov/vuln/detail/CVE-2022-2514ghsa
News mentions
0No linked articles in our index yet.