Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 SwiftURL

GHSA-6jj5-j4j8-8473

LeafKit's HTML escaping may be skipped for Collection values, enabling XSS

Also known asCVE-2026-28499
Published
Mar 16, 2026
Updated
May 7, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk18th percentile+0.25%
0.00%0.26%0.51%0.77%0.0%0.0%0.0%0.3%Apr 26Jun 26Jun 26

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

Blast Radius

1 pkg affected
📦github.com/vapor/leaf-kit

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects SwiftURL packages — download data is not available via public APIs for these ecosystems.

Description

Summary

LeafKit HTML-escaping is not working correctly when a template prints a collection (Array / Dictionary) via #(value). This can result in XSS, allowing potentially untrusted input to be rendered unescaped.

Details

LeafKit attempts to escape expressions during serialization, but due to LeafData.htmlEscaped()'s implementation, when the escaped type's conversion to String is marked as .ambiguous (as it is the case for Arrays and Dictionaries), an unescaped self is returned.

Note: I recommend first looking at the POC, before taking a look at the details below, as it is simple. In the detailed, verbose analysis below, I explored the functions involved in more detail, in hopes that it will help you understand and locate this issue.

The issue's detailed analysis:

  1. Leaf expression serialization eventually reaches LeafSerializer's serialize private function below. This is where the leafData is .htmlEscaped(), and then serialized.

https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafSerialize/LeafSerializer.swift#L60-L66

  1. The LeafData.htmlEscaped() method uses the LeafData.string computed property to convert itself to a string. Then, it calls the htmlEscaped() method on it. However, if the string conversion fails, notice that an unescaped, unsafe self is returned (line 324 below):

https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L321-L328

  1. Regarding why .string may return nil, if the escaped value is not a string already, a convesion is attempted, which may fail.

https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L211-L216

In this specific case, the conversion fails at line 303 below, when conversion.is >= level is checked. The check fails because .array and .dictionary conversions to .string are deemed .ambiguous. If we forcefully allow ambiguous conversions, the vulnerability disappears, as the conversion is successful.

https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L295-L319

  1. Coming back to LeafSerializer's serialize private method, we are now interested in finding out what happens after LeafData.htmlEscaped() returns self. Recall from 1. that the output was then .serialized(). Thus, the unescaped LeafData follows the normal serialization path, as if it were HTML-escaped. More specifically, serialization is done here, where .map / .mapValues is called, unsafely serializing each element of the dictionary.

PoC

<!-- _Complete instructions, including specific configuration details, to reproduce the vulnerability._ -->

In a new Vapor project created with vapor new poc -n --leaf, use a simple leaf template like the following:

<!doctype html>
<html>
    <body>
    <h1>#(username)</h1>
      <h2>someDict:</h2>
      <p>#(someDict)</p>
  </body>
</html>

And the following routes.swift:

import Vapor

struct User: Encodable {
    var username: String
    var someDict: [String: String]
}

func routes(_ app: Application) throws {
    app.get { req async throws in
        try await req.view.render("index", User(
            username: "Escaped XSS - <img src=x onerror=alert(1)>",
            someDict: ["<img src=x onerror=alert(1337)>":"<img src=x onerror=alert(31337)>"]
        ))
    }
}

By running and accessing the server in a browser, XSS should be triggered twice (with alert(1337) and alert(31337)). var someDict: [String: String] could also be replaced with an array / dictionary of a different type, such as another Encodable stuct.

Also note that, in a real concerning scenario, the array / dictionary would contain (i.e. reflect) data inputted by the user.

Impact

This is a cross-site scripting (XSS) vulnerability in rendered Leaf templates. Vapor/Leaf applications that render user-controlled data inside arrays or dictionaries using #(value) may be impacted.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦SwiftURLgithub.com/vapor/leaf-kitall versions1.14.2

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/vapor/leaf-kit. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update github.com/vapor/leaf-kit to 1.14.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-6jj5-j4j8-8473 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-6jj5-j4j8-8473 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-6jj5-j4j8-8473. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### Summary LeafKit HTML-escaping is not working correctly when a template prints a collection (Array / Dictionary) via `#(value)`. This can result in XSS, allowing potentially untrusted input to be rendered unescaped. ### Details LeafKit attempts to escape expressions during serialization, but due to [`LeafData.htmlEscaped()`](https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L322)'s implementation, when the escaped type's conversion to `String` is marked as `.ambiguous` (as it is the case for Arrays and Dictionaries), an
O3 Security · Impact-Aware SCA

Is GHSA-6jj5-j4j8-8473 in your dependencies?

O3 detects GHSA-6jj5-j4j8-8473 across SwiftURL dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-6jj5-j4j8-8473: leaf-kit Cross-Site Scripting | O3 Security