CVE-2026-28499: Github.com/vapor/leaf kit Cross-Site Scripting Vulnerability

MODERATE
Advisory Identifier: GHSA-6jj5-j4j8-8473
CWE-116 CWE-80 CWE-79
Published
Advisory Summary

CVE-2026-28499 is a MODERATE cross-site scripting vulnerability (CWE-116: Improper Encoding or Escaping of Output) affecting Github.com/vapor/leaf kit libraries in the SwiftURL ecosystem. The issue involves leafkit's html escaping may be skipped for collection values, enabling xss. Remediation is available by upgrading affected packages to version 1.14.2.

CVE-2026-28499 at a Glance

CVE Identifier CVE-2026-28499
GHSA Identifier GHSA-6jj5-j4j8-8473
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-116: Improper Encoding or Escaping of Output
Affected Product Github.com/vapor/leaf kit
Package Ecosystem SwiftURL
Published Date
Last Updated

How to Fix CVE-2026-28499

To remediate this vulnerability, upgrade all affected Github.com/vapor/leaf kit dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/vapor/leaf-kit fixed: 1.14.2
Upgrade github.com/vapor/leaf-kit to 1.14.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/vapor/leaf-kit

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-28499, inspect your project dependencies for Github.com/vapor/leaf kit:

inspect dependencies for github.com/vapor/leaf-kit

Analyze the terminal output to locate any version of Github.com/vapor/leaf kit in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.14.2), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-116: Improper Encoding or Escaping of Output, where the product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. as a result, the intended structure of the message is not preserved. In Github.com/vapor/leaf kit, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 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 2. 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 3. 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`](https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L525-L535). 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 5. 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](https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafDataStorage.swift#L52-L63), 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: ```html <!doctype html> <html> <body> <h1>#(username)</h1> <h2>someDict:</h2> <p>#(someDict)</p> </body> </html> ``` And the following `routes.swift`: ```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.

CVE-2026-28499 Affected Github.com/vapor/leaf kit Versions

1 component(s)
Fixed Versions:
1.14.2
Product / Package Ecosystem Affected Versions Fixed Version
github.com/vapor/leaf-kit
SwiftURL
< 1.14.2
1.14.2

References

Frequently Asked Questions

What is CVE-2026-28499?
CVE-2026-28499 is a security vulnerability in Github.com/vapor/leaf kit involving LeafKit's HTML escaping may be skipped for Collection values, enabling XSS under CWE-116: Improper Encoding or Escaping of Output.
Which Github.com/vapor/leaf kit versions are affected?
Affected versions include releases prior to 1.14.2 across affected packages: github.com/vapor/leaf-kit.
What version fixes CVE-2026-28499?
Upgrading to 1.14.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/vapor/leaf-kit affected?
Yes, github.com/vapor/leaf-kit is affected in releases prior to 1.14.2. Upgrade your dependency to 1.14.2 or later.
What CWE is associated with CVE-2026-28499?
CVE-2026-28499 is classified under CWE-116: Improper Encoding or Escaping of Output (The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved.).
How can I check SwiftURL dependencies for CVE-2026-28499?
Run the dependency tree command: 'inspect dependencies for github.com/vapor/leaf-kit'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-6jj5-j4j8-8473",
  "modified" : "2026-05-07T13:24:10Z",
  "published" : "2026-03-16T16:22:56Z",
  "aliases" : [ "CVE-2026-28499" ],
  "summary" : "LeafKit's HTML escaping may be skipped for Collection values, enabling XSS",
  "details" : "### Summary\nLeafKit 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.\n\n### Details\nLeafKit 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 unescaped `self` is returned.\n\n> **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.\n\n#### The issue's detailed analysis:\n1. Leaf expression serialization eventually reaches `LeafSerializer`'s `serialize` private function below.  This is where the `leafData` is `.htmlEscaped()`, and then serialized.\n\nhttps://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafSerialize/LeafSerializer.swift#L60-L66\n\n2. 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):\n\nhttps://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L321-L328\n\n\n3. Regarding why `.string` may return nil, if the escaped value is not a string already, a convesion is attempted, which may fail.\n\nhttps://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L211-L216\n\nIn 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`](https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L525-L535). If we forcefully allow ambiguous conversions, the vulnerability disappears, as the conversion is successful.\n\nhttps://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafData.swift#L295-L319\n\n5. 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](https://github.com/vapor/leaf-kit/blob/8ff06839d8b3ddf74032d2ade01e3453eb556d30/Sources/LeafKit/LeafData/LeafDataStorage.swift#L52-L63), where `.map` / `.mapValues` is called, unsafely serializing each element of the dictionary.\n\n### PoC\n<!-- _Complete instructions, including specific configuration details, to reproduce the vulnerability._ -->\n\nIn a new Vapor project created with `vapor new poc -n --leaf`, use a simple leaf template like the following:\n```html\n<!doctype html>\n<html>\n    <body>\n    <h1>#(username)</h1>\n      <h2>someDict:</h2>\n      <p>#(someDict)</p>\n  </body>\n</html>\n```\n\nAnd the following `routes.swift`:\n```swift\nimport Vapor\n\nstruct User: Encodable {\n    var username: String\n    var someDict: [String: String]\n}\n\nfunc routes(_ app: Application) throws {\n    app.get { req async throws in\n        try await req.view.render(\"index\", User(\n            username: \"Escaped XSS - <img src=x onerror=alert(1)>\",\n            someDict: [\"<img src=x onerror=alert(1337)>\":\"<img src=x onerror=alert(31337)>\"]\n        ))\n    }\n}\n\n```\n\nBy 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.\n\nAlso note that, in a real concerning scenario, the array / dictionary would contain (i.e. reflect) data inputted by the user.\n\n### Impact\nThis 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.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "SwiftURL",
      "name" : "github.com/vapor/leaf-kit"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.14.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/vapor/leaf-kit/security/advisories/GHSA-6jj5-j4j8-8473"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-28499"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/vapor/leaf-kit/commit/6044b844caa858a0c5f2505ac166f5a057c990dc"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/vapor/leaf-kit"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/vapor/leaf-kit/releases/tag/1.14.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-116", "CWE-79", "CWE-80" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-16T16:22:56Z",
    "nvd_published_at" : "2026-03-18T02:16:24Z"
  }
}