VYPR
Moderate severityNVD Advisory· Published Mar 18, 2026· Updated Mar 18, 2026

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

CVE-2026-28499

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

LeafKit prior to 1.14.2 improperly handles HTML escaping for collection values, enabling cross-site scripting (XSS) via untrusted input rendered unescaped.

Vulnerability

LeafKit, a Swift-inspired templating language, prior to version 1.14.2, fails to apply HTML escaping correctly when a template prints a collection (Array/Dictionary) using the #(value) syntax. The root cause is in the LeafData.htmlEscaped() method, which returns an unescaped self when the conversion to string is ambiguous, as is the case for collections [1][2].

Exploitation

An attacker can exploit this by providing untrusted input that is rendered as a collection in a LeafKit template. No special privileges are required; the vulnerability can be triggered through any template that outputs user-controlled data via #(value) [1][2].

Impact

Successful exploitation leads to cross-site scripting (XSS), allowing the attacker to inject arbitrary HTML and JavaScript into the rendered page, potentially leading to session theft, defacement, or other malicious actions [1].

Mitigation

The issue is fixed in LeafKit version 1.14.2 [1][2][4]. Users should upgrade to this version or later to prevent XSS attacks. No workaround is available.

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/vapor/leaf-kitSwiftURL
< 1.14.21.14.2

Affected products

2
  • LeafKit/LeafKitllm-create
    Range: <1.14.2
  • vapor/leaf-kitv5
    Range: < 1.14.2

Patches

1
6044b844caa8

Merge commit from fork

https://github.com/vapor/leaf-kitGwynne RaskindMar 14, 2026via ghsa
3 files changed · +16 9
  • Sources/LeafKit/LeafData/LeafDataStorage.swift+1 6 modified
    @@ -64,12 +64,7 @@ indirect enum LeafDataStorage: Equatable, CustomStringConvertible, Sendable {
         
         /// Final serialization to a shared buffer
         func serialize(buffer: inout ByteBuffer) throws {
    -        switch self {
    -        case .bool, .int, .double, .string, .optional, .array, .dictionary:
    -            try buffer.writeString(self.serialize(), encoding: LeafConfiguration.encoding)
    -        case .data(let d):
    -            buffer.writeData(d)
    -        }
    +        try buffer.writeString(self.serialize(), encoding: LeafConfiguration.encoding)
         }
         
         // MARK: - Equatable Conformance
    
  • Sources/LeafKit/LeafData/LeafData.swift+2 2 modified
    @@ -320,7 +320,7 @@ public struct LeafData:
     
         /// Return a HTML-escaped version of this data if it can be converted to a string.
         func htmlEscaped() -> LeafData {
    -        guard let string = self.string else {
    +        guard let string = self.convert(to: .string, .ambiguous).string else {
                 return self
             }
     
    @@ -334,7 +334,7 @@ public struct LeafData:
     enum DataConvertible: Int, Equatable, Comparable {
         /// Not implicitly convertible automatically
         case ambiguous = 0
    -    /// A coercioni with a clear meaning in one direction
    +    /// A coercion with a clear meaning in one direction
         case coercible = 1
         /// A conversion with a well-defined bi-directional casting possibility
         case castable = 2
    
  • Tests/LeafKitTests/TagTests.swift+13 1 modified
    @@ -12,6 +12,18 @@ final class TagTests: XCTestCase {
             try XCTAssertEqual(render(template, ["name": "<h1>Alex</h1>\"\'"]), expected)
         }
     
    +    func testOtherThingsWithHTMLEntities() throws {
    +        try XCTAssertEqual(render("#(foo)", ["foo": .data(Data([0x3c, 0x3e, 0xc3, 0xff]))]), "")
    +        try XCTAssertEqual(render("#(foo)", ["foo": .data(Data([0x3c, 0x3e, 0xc3, 0xb7]))]), "&lt;&gt;÷")
    +        try XCTAssertEqual(render("#(foo)", ["foo": ["<img src=x onerror=alert(1337)>"]]), "[&quot;&lt;img src=x onerror=alert(1337)&gt;&quot;]")
    +        try XCTAssertEqual(render("#(foo)", ["foo": ["<img src=x onerror=alert(1337)>": "<img src=x onerror=alert(1337)>"]]), "[&lt;img src=x onerror=alert(1337)&gt;: &quot;&lt;img src=x onerror=alert(1337)&gt;&quot;]")
    +
    +        try XCTAssertThrowsError(render("#unsafeHTML(foo)", ["foo": .data(Data([0x3c, 0x3e, 0xc3, 0xff]))]))
    +        try XCTAssertEqual(render("#unsafeHTML(foo)", ["foo": .data(Data([0x3c, 0x3e, 0xc3, 0xb7]))]), "<>÷")
    +        try XCTAssertThrowsError(render("#unsafeHTML(foo)", ["foo": ["<img src=x onerror=alert(1337)>"]]))
    +        try XCTAssertThrowsError(render("#unsafeHTML(foo)", ["foo": ["<img src=x onerror=alert(1337)>": "<img src=x onerror=alert(1337)>"]]))
    +    }
    +
         func testUnsafeTag() throws {
             let template = """
             #unsafeHTML(name)
    @@ -255,7 +267,7 @@ final class TagTests: XCTestCase {
             """
     
             let expected = """
    -        dumpContext should output debug description [value: "12345"]
    +        dumpContext should output debug description [value: &quot;12345&quot;]
             """
     
             try XCTAssertEqual(render(template, data), expected)
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.