Safeguard
Vulnerability Analysis

Jinja2 xmlattr filter XSS (CVE-2024-22195)

CVE-2024-22195 lets attacker-controlled dict keys bypass Jinja2's xmlattr escaping for XSS. Learn affected versions, CVSS/EPSS context, and fixes.

Priya Mehta
DevSecOps Engineer
7 min read

A quiet fix in a template engine's attribute filter turned out to hide a real cross-site scripting hole. CVE-2024-22195 affects the xmlattr filter in Jinja, the templating engine that powers Flask, Ansible, Django add-ons, and countless internal admin tools and code-generation pipelines. If an application passes user-controlled data as the keys of a dictionary rendered through xmlattr, an attacker can smuggle extra HTML attributes — including onmouseover, onerror, or style-based payloads — straight past Jinja's autoescaping and into the rendered page. For any app that reflects that output to other users, this is a stored or reflected XSS vector, and it's a good reminder that "escaping" a template value doesn't help if the structure around the value is itself attacker-controlled.

This is a jinja2 xss vulnerability with a subtle root cause: it's not a missing | escape call, it's a filter that never anticipated spaces (and later, other special characters) appearing inside attribute names rather than attribute values.

What's actually broken

Jinja's xmlattr filter takes a dict and renders it as a string of HTML/XML attributes, e.g. {{ {"class": "foo"} | xmlattr }} produces class="foo". The filter correctly escapes attribute values, but prior to the fix it did no validation on attribute keys. HTML and XML attributes cannot legally contain whitespace — a space always starts a new attribute. So if an attacker controls a key like "foo onmouseover=alert(1)", the filter happily emits:

foo onmouseover=alert(1)=""

which a browser parses as two separate attributes, one of them a live event handler. Because this bypasses Jinja's autoescape mechanism entirely (the key is never treated as "text that needs escaping," it's treated as trusted markup structure), any downstream blacklist or attribute-name validation an application relies on can be bypassed too.

Using xmlattr with attacker-controlled keys is an unusual pattern — most templates use it to render a fixed, developer-defined set of attribute names with user-controlled values, which was never at risk. But the pattern does show up in generic "render this dict as attributes" helpers, form-builder libraries, and templating layers built on top of Jinja (Ansible, config generators, static-site tools, internal dashboards) where a dict assembled from user input can end up being passed straight into xmlattr without anyone auditing the key names.

Affected versions and severity

  • Affected: all Jinja / Jinja2 releases prior to 3.1.3.
  • Initial fix: Jinja 3.1.3 (released January 10, 2024), tracked as CVE-2024-22195 / GHSA-h5c8-rqwp-cp95.
  • Important follow-up: the 3.1.3 patch only blocked spaces in keys. It missed other characters (/, >, =) that can also break attribute syntax and re-enable injection. This gap was tracked separately as CVE-2024-34064 / GHSA-h75v-3vvj-5mfj, published May 5, 2024, and fully resolved in Jinja 3.1.4. Teams that patched to 3.1.3 and stopped there are still exposed to the residual injection vector.

CVSS: Scored differently depending on the source — GitHub's advisory rates it 5.4 (Medium), CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N; NVD rates it slightly higher at 6.1 (Medium) with a scope-changed vector, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N. Both reflect a network-exploitable, low-complexity, no-privilege-required issue whose confidentiality/integrity impact is capped by the need for user interaction (someone has to view the poisoned page) — classic XSS scoring. The CWE classification is CWE-79 (Improper Neutralization of Input During Web Page Generation).

EPSS: roughly 0.15% predicted 30-day exploitation probability, in the ~35th percentile of scored CVEs — consistent with a real but narrow-precondition bug rather than something under active mass exploitation.

CISA KEV: Not listed on the CISA Known Exploited Vulnerabilities catalog as of this writing. There is no public evidence of in-the-wild exploitation; this is a proactive-patch situation, not an active-incident one.

Because Jinja2 is a transitive dependency of an enormous number of Python web frameworks and tools (Flask and its ecosystem, Ansible, MkDocs, Airflow plugins, code generators, and countless internal Flask/FastAPI services that still render Jinja templates), the practical exposure of any given CVSS-6.1 bug in it is really a function of how each downstream app uses xmlattr — which is exactly the kind of thing that's hard to answer from a dependency manifest alone.

Timeline

  • Pre-2024: xmlattr ships without key validation across all Jinja/Jinja2 2.x and 3.x releases.
  • January 10, 2024: Jinja 3.1.3 released, fixing the space-in-keys injection; CVE-2024-22195 / GHSA-h5c8-rqwp-cp95 published.
  • Following weeks: Linux distributions and package ecosystems (Debian, Red Hat, Ubuntu, etc.) backport the fix into their tracked package versions.
  • May 5, 2024: Maintainers determine the 3.1.3 fix was incomplete — other characters (/, >, =) still allowed attribute injection. Jinja 3.1.4 ships the complete fix; CVE-2024-34064 / GHSA-h75v-3vvj-5mfj published, explicitly reclassifying "user-controlled keys into xmlattr" as an unsupported/insecure pattern regardless of patch level.
  • Ongoing: advisory continues to surface in dependency and container scanners as organizations reconcile pinned Jinja2 versions across services, CI images, and vendored dependencies (e.g., bundled copies inside other packages such as nlohmann/json-adjacent tooling and various SDKs).

Remediation

  1. Upgrade to Jinja 3.1.4 or later, not just 3.1.3. If your lockfiles, requirements.txt, poetry.lock, or base container images pin Jinja2 at exactly 3.1.3, treat that as still vulnerable to CVE-2024-34064 and bump further.
    pip install --upgrade "Jinja2>=3.1.4"
    
  2. Audit every call site that passes dynamic dict keys to xmlattr. Search your templates and Python code for | xmlattr and for any dict()/**kwargs construction feeding it where the keys — not just values — originate from request parameters, form data, query strings, config uploads, or any other user-influenced source.
  3. Never trust attribute names from user input, patched Jinja or not. The maintainers now explicitly call this an unsupported pattern: even after 3.1.4, prefer a fixed, developer-defined allowlist of attribute names and only let user input populate values.
  4. Check transitive exposure, not just direct dependencies. Jinja2 rides in via Flask, Ansible, MkDocs, and many internal tools; a vulnerable copy can be pinned two or three layers down in your dependency graph, or vendored inside a container base image, without showing up in a top-level requirements.txt scan.
  5. Re-scan after upgrading. Confirm SBOMs and container images reflect Jinja2>=3.1.4 across every service, build image, and CI runner — not just the service you remembered to patch first.
  6. Add regression coverage for any template code that renders user-influenced attribute dictionaries, asserting that whitespace and / > = characters in keys are rejected or encoded rather than passed through.

How Safeguard Helps

Dependency scanners will flag "Jinja2 < 3.1.4" everywhere it appears, but most of those instances are inert — the vulnerable code path only matters if your application actually feeds user-controlled keys into xmlattr. Safeguard's reachability analysis traces call graphs from your entry points through to that specific filter usage, so you can tell in minutes whether a given finding is exploitable in your code or just noise from a transitive dependency, cutting through exactly the kind of alert fatigue this CVE tends to generate. Griffin, Safeguard's AI security analyst, reads the actual template and view code around each xmlattr call to judge whether the surrounding dict keys are attacker-influenced, and drafts the risk narrative a human reviewer would otherwise have to write by hand. Continuous SBOM generation and ingestion keep every vendored, transitive, and container-layer copy of Jinja2 inventoried across your fleet, so a patch cycle like 3.1.3 → 3.1.4 doesn't quietly miss a service three dependencies deep. And where the fix really is "just bump the version," Safeguard opens an auto-fix pull request with the corrected pin so your team can merge and move on instead of chasing the upgrade manually across dozens of repositories.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.