GHSA-6jj5-j4j8-8473
LeafKit's HTML escaping may be skipped for Collection values, enabling XSS
EPSS Exploitation Probability
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
github.com/vapor/leaf-kitReal-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:
- Leaf expression serialization eventually reaches
LeafSerializer'sserializeprivate function below. This is where theleafDatais.htmlEscaped(), and then serialized.
- The
LeafData.htmlEscaped()method uses theLeafData.stringcomputed property to convert itself to a string. Then, it calls thehtmlEscaped()method on it. However, if the string conversion fails, notice that an unescaped, unsafeselfis returned (line 324 below):
- Regarding why
.stringmay return nil, if the escaped value is not a string already, a convesion is attempted, which may fail.
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.
- Coming back to
LeafSerializer'sserializeprivate method, we are now interested in finding out what happens afterLeafData.htmlEscaped()returns self. Recall from1.that the output was then.serialized(). Thus, the unescapedLeafDatafollows the normal serialization path, as if it were HTML-escaped. More specifically, serialization is done here, where.map/.mapValuesis 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
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦SwiftURL | github.com/vapor/leaf-kit | all versions | 1.14.2 |
Detection & mitigation playbook
Open-source dependencyDetect
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.
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.
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.
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
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.