PT-2026-25815 · Swifturl · Leafkit
Published
2026-03-16
·
Updated
2026-03-16
·
CVE-2026-28499
CVSS v4.0
6.9
| Vector | AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N |
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
'sLeafSerializer
private function below. This is where theserialize
isleafData
, and then serialized..htmlEscaped()
- The
method uses theLeafData.htmlEscaped()
computed property to convert itself to a string. Then, it calls theLeafData.string
method on it. However, if the string conversion fails, notice that an unescaped, unsafehtmlEscaped()
is returned (line 324 below):self
- Regarding why
may return nil, if the escaped value is not a string already, a convesion is attempted, which may fail..string
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
'sLeafSerializer
private method, we are now interested in finding out what happens afterserialize
returns self. Recall fromLeafData.htmlEscaped()
that the output was then1.
. Thus, the unescaped.serialized()
follows the normal serialization path, as if it were HTML-escaped. More specifically, serialization is done here, whereLeafData
/.map
is called, unsafely serializing each element of the dictionary..mapValues
PoC
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.Fix
XSS
Improper Encoding or Escaping of Output
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Leafkit