← All issues

[9] SubtleCrypto RSA import OOB read from missing ASN.1 bounds check

Severity: Medium | Component: WebCore SubtleCrypto | bbec6de

Rated Medium because the observable effect is a reliable renderer crash (deterministic SIGTRAP in hardened builds) reachable from any web page via crypto.subtle.importKey(), with no commit-backed evidence of information disclosure — escalation to an OOB heap read in non-hardened builds is projected but the read data flows only into CCRSACryptorImport which rejects it, limiting info-leak potential.

CryptoKeyRSA::importSpki() and CryptoKeyRSA::importPkcs8() now check keyData.size() < headerSize after the final ASN.1 header size computation, before calling keyData.subspan(headerSize).

Source/WebCore/crypto/cocoa/CryptoKeyRSAMac.cpp

if (keyData.size() < headerSize + 1)
return nullptr;
headerSize += bytesUsedToEncodedLength(keyData[headerSize]) + sizeof(InitialOctet);
+ if (keyData.size() < headerSize)
+ return nullptr;
 
CCRSACryptorRef ccPublicKey = nullptr;
auto dataAfterHeader = keyData.subspan(headerSize);
if (keyData.size() < headerSize + 1)
return nullptr;
headerSize += bytesUsedToEncodedLength(keyData[headerSize]);
+ if (keyData.size() < headerSize)
+ return nullptr;
 
CCRSACryptorRef ccPrivateKey = nullptr;
auto dataAfterHeader = keyData.subspan(headerSize);

LayoutTests/crypto/subtle/rsa-import-pkcs8-truncated-key.html

+ // headerSize computation:
+ // 5. headerSize = 21 + 3 = 24
+ // 6. subspan(24) on 22-byte buffer -> CRASH (before fix)
+ var truncatedPkcs8Key = hexStringToUint8Array("30000000000000000000000000000000000000000082");
+ shouldReject('crypto.subtle.importKey("pkcs8", truncatedPkcs8Key, {name: "RSA-OAEP", hash: "sha-256"}, extractable, ["decrypt"])');

Two identical one-line additions in importSpki() and importPkcs8(). Each adds if (keyData.size() < headerSize) return nullptr; after the final headerSize += bytesUsedToEncodedLength(...) computation and before the keyData.subspan(headerSize) call. The existing intermediate bounds checks on headerSize remain unchanged.

ASN.1 uses a tag-length-value (TLV) encoding. Length fields can be "short form" (single byte ≤ 127) or "long form" (first byte = 0x80 + N, followed by N bytes encoding the actual length). bytesUsedToEncodedLength() returns the total number of bytes consumed by the length field — for long-form, this is (firstByte - 128) + 1. WebKit's RSA key import functions manually strip the ASN.1 header by incrementally computing a headerSize offset, then slice the input buffer at that offset using std::span::subspan().

In hardened C++ standard library builds, subspan() with an out-of-range offset triggers a deterministic trap (EXC_BREAKPOINT / SIGTRAP). In non-hardened builds, subspan() with an out-of-range offset silently creates a span starting past the buffer end.

Missing final bounds check after incremental ASN.1 header size computation, before span slicing.

Both importSpki() and importPkcs8() performed intermediate bounds checks on headerSize as it was built up from ASN.1 length fields, but the final increment — adding bytesUsedToEncodedLength(keyData[headerSize]) — had no subsequent check. A crafted key with a large length-of-length indicator at the final position (e.g., 0x82 means 2 additional length bytes, so the function returns 3) could push headerSize past keyData.size(). The test case demonstrates this directly: a 22-byte PKCS#8 key where the final 0x82 byte causes headerSize to reach 24, and subspan(24) on a 22-byte buffer crashes.

🔒

Explores the exploitability boundaries of this OOB access and what limits escalation beyond a crash

Subscribe to read more

🔒

Multiple audit patterns identified for ASN.1 parsing across WebKit's crypto subsystem, with concrete search targets

Subscribe to read more