VYPR
Moderate severityNVD Advisory· Published Feb 20, 2026· Updated Feb 24, 2026

Leaf-kit html escaping does not work on characters that are part of extended grapheme cluster

CVE-2026-27120

Description

Leafkit is a templating language with Swift-inspired syntax. Prior to 1.4.1, htmlEscaped in leaf-kit will only escape html special characters if the extended grapheme clusters match, which allows bypassing escaping by using an extended grapheme cluster containing both the special html character and some additional characters. In the case of html attributes, this can lead to XSS if there is a leaf variable in the attribute that is user controlled. This vulnerability is fixed in 1.4.1.

AI Insight

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

Leaf-kit before 1.4.1 fails to escape HTML special characters when they are part of extended grapheme clusters, enabling XSS in attributes.

CVE-2026-27120 is a vulnerability in leaf-kit, a templating language with Swift-inspired syntax, prior to version 1.4.1. The htmlEscaped escaping function relies on Swift's string comparison which operates on extended grapheme clusters, rather than individual Unicode scalar values. This causes characters such as the double quote (") to not be replaced when they are combined with combining characters like the acute accent (U+0301) into a single grapheme cluster, allowing the escaping mechanism to be bypassed [1][3].

Exploitation requires an attacker to control a user-supplied value placed inside an HTML attribute in a template. By crafting input that uses an extended grapheme cluster containing a special HTML character (e.g., " followed by a combining character), the attacker can break out of the attribute without the quote being escaped, thereby injecting arbitrary HTML and JavaScript [3]. The attack is described as primarily affecting attribute contexts because the bypass relies on the escaping being skipped for a character that is part of a larger cluster [3].

Successful exploitation results in stored or reflected cross-site scripting (XSS) within the context of the vulnerable application, allowing attackers to execute scripts, steal cookies, or perform actions on behalf of the victim [1][3].

The vulnerability has been fixed in leaf-kit version 1.4.1. The fix, visible in commit 8919e39476c3a4ba05c28b71546bb9195f87ef34, replaces the previous replacing or replacingOccurrences(of:) calls with a reduction over unicodeScalars, ensuring that each Unicode scalar is escaped independently and preventing bypass via grapheme clusters [4]. Users are strongly advised to upgrade to version 1.4.1 or later [1].

AI Insight generated on May 19, 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.4.11.4.1

Affected products

2

Patches

1
8919e39476c3

Merge commit from fork

https://github.com/vapor/leaf-kitPaul ToffoloniFeb 18, 2026via ghsa
2 files changed · +34 16
  • Sources/LeafKit/String+HTMLEscape.swift+9 16 modified
    @@ -1,22 +1,15 @@
    -import Foundation
    -
     extension String {
         /// Escapes HTML entities in a `String`.
         public func htmlEscaped() -> String {
    -        if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
    -            self
    -                .replacing("&", with: "&amp;")
    -                .replacing("\"", with: "&quot;")
    -                .replacing("'", with: "&#39;")
    -                .replacing("<", with: "&lt;")
    -                .replacing(">", with: "&gt;")
    -        } else {
    -            self
    -                .replacingOccurrences(of: "&", with: "&amp;")
    -                .replacingOccurrences(of: "\"", with: "&quot;")
    -                .replacingOccurrences(of: "'", with: "&#39;")
    -                .replacingOccurrences(of: "<", with: "&lt;")
    -                .replacingOccurrences(of: ">", with: "&gt;")
    +        self.unicodeScalars.reduce(into: "") { result, scalar in
    +            switch scalar {
    +            case "&": result += "&amp;"
    +            case "\"": result += "&quot;"
    +            case "'": result += "&#39;"
    +            case "<": result += "&lt;"
    +            case ">": result += "&gt;"
    +            default: result.unicodeScalars.append(scalar)
    +            }
             }
         }
     }
    
  • Tests/LeafKitTests/HTMLEscapeTests.swift+25 0 modified
    @@ -9,6 +9,31 @@ final class HTMLEscapeTests: XCTestCase {
             XCTAssertEqual("abc&".htmlEscaped(), "abc&amp;")
         }
     
    +    func testExtendedGraphemeClusterBypass() {
    +        let quoteWithCombining = "\u{0022}\u{0301}"  // "́
    +        let escaped = quoteWithCombining.htmlEscaped()
    +
    +        XCTAssertEqual(escaped, "&quot;\u{0301}")
    +
    +        let maliciousInput = "\"\u{0301}=1 autofocus tabindex=0 onfocus=alert(1)"
    +        let escapedMalicious = maliciousInput.htmlEscaped()
    +
    +        XCTAssertFalse(escapedMalicious.contains("\"\u{0301}"))
    +        XCTAssertTrue(escapedMalicious.unicodeScalars.starts(with: "&quot;".unicodeScalars))
    +
    +        let ampersandWithCombining = "&\u{0301}"  // &́
    +        XCTAssertEqual(ampersandWithCombining.htmlEscaped(), "&amp;\u{0301}")
    +
    +        let lessThanWithCombining = "<\u{0301}"  // <́
    +        XCTAssertEqual(lessThanWithCombining.htmlEscaped(), "&lt;\u{0301}",)
    +
    +        let greaterThanWithCombining = ">\u{0301}"  // >́
    +        XCTAssertEqual(greaterThanWithCombining.htmlEscaped(), "&gt;\u{0301}")
    +
    +        let apostropheWithCombining = "'\u{0301}"  // '́
    +        XCTAssertEqual(apostropheWithCombining.htmlEscaped(), "&#39;\u{0301}")
    +    }
    +
         #if !os(Android)
         func testShortStringNoReplacements() {
             let string = "abcde12345"
    

Vulnerability mechanics

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

References

4

News mentions

0

No linked articles in our index yet.