Every CVE Search/ Get API key

CVE-2026-28499

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

6.9 MediumFix available

LeafKit is a templating language with Swift-inspired syntax. Prior to version 1.14.2, HTML escaping doesn't work correctly when a template prints a collection (Array / Dictionary) via #(value). This can result in XSS, allowing potentially untrusted input to be rendered unescaped. Version 1.14.2 fixes the issue.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
None
Integrity
None
Availability
None

Exploit likelihood

0.28%Up 0.2 points since Mar 18, 2026
90%50%10%1%

What each source reports

CVE List Score used 6.9

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

LeafKit is a templating language with Swift-inspired syntax. Prior to version 1.14.2, HTML escaping doesn't work correctly when a template prints a collection (Array / Dictionary) via #(value). This can result in XSS, allowing potentially untrusted input to be rendered unescaped. Version 1.14.2 fixes the issue.

Record
CVE-2026-28499
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N
Weakness
CWE-79, CWE-80, CWE-116
Updated
2026-11-02
EUVDEUVD-2026-12696 6.9

LeafKit is a templating language with Swift-inspired syntax. Prior to version 1.14.2, HTML escaping doesn't work correctly when a template prints a collection (Array / Dictionary) via #(value). This can result in XSS, allowing potentially untrusted input to be rendered unescaped. Version 1.14.2 fixes the issue.

Record
EUVD-2026-12696
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-6jj5-j4j8-8473 6.9

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

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…

Record
GHSA-6jj5-j4j8-8473
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N
Weakness
CWE-79, CWE-80, CWE-116
Updated
2026-11-02
NVD 6.9

LeafKit is a templating language with Swift-inspired syntax. Prior to version 1.14.2, HTML escaping doesn't work correctly when a template prints a collection (Array / Dictionary) via #(value). This can result in XSS, allowing potentially untrusted input to be rendered unescaped. Version 1.14.2 fixes the issue.

Record
CVE-2026-28499
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-79, CWE-80, CWE-116
Updated
2026-11-02
OSV 6.9

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

LeafKit is a templating language with Swift-inspired syntax. Prior to version 1.14.2, HTML escaping doesn't work correctly when a template prints a collection (Array / Dictionary) via #(value). This can result in XSS, allowing potentially untrusted input to be rendered unescaped. Version 1.14.2 fixes the issue.

Record
CVE-2026-28499
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N
Weakness
CWE-116, CWE-79, CWE-80
Updated
2026-11-02
OSVGHSA-6jj5-j4j8-8473 6.9

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

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…

Record
GHSA-6jj5-j4j8-8473
CVSS 4.0
6.9 MediumCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N
Weakness
CWE-116, CWE-79, CWE-80
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
github.com/vapor/leaf-kit
SwiftURL
before 1.14.2
1.14.2

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-28499 is a medium vulnerability (CVSS 6.9) in Vapor Leafkit, published March 16, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.28%, the 20th percentile. Fixed in github.com/vapor/leaf-kit 1.14.2.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open