LeafKit's HTML escaping may be skipped for Collection values, enabling XSS
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/vapor/leaf-kitSwiftURL | < 1.14.2 | 1.14.2 |
Affected products
2- vapor/leaf-kitv5Range: < 1.14.2
Patches
16044b844caa8Merge commit from fork
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]))]), "<>÷") + try XCTAssertEqual(render("#(foo)", ["foo": ["<img src=x onerror=alert(1337)>"]]), "["<img src=x onerror=alert(1337)>"]") + try XCTAssertEqual(render("#(foo)", ["foo": ["<img src=x onerror=alert(1337)>": "<img src=x onerror=alert(1337)>"]]), "[<img src=x onerror=alert(1337)>: "<img src=x onerror=alert(1337)>"]") + + 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: "12345"] """ 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- github.com/advisories/GHSA-6jj5-j4j8-8473ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28499ghsaADVISORY
- github.com/vapor/leaf-kit/commit/6044b844caa858a0c5f2505ac166f5a057c990dcghsax_refsource_MISCWEB
- github.com/vapor/leaf-kit/releases/tag/1.14.2ghsax_refsource_MISCWEB
- github.com/vapor/leaf-kit/security/advisories/GHSA-6jj5-j4j8-8473ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.