CVE-2026-16359
Overview
Background
- CENC (Common Encryption)
- the MP4 scheme in which media samples are encrypted with a per-sample initialization vector and key ID, decrypted here by the Clear Key CDM.
- `tenc` box
- the CENC track-encryption metadata atom inside an MP4
sinf, carrying the default per-sample IV size and default key ID. - `SinfParser::ParseTenc`
- the Gecko demuxer routine that reads the
tencbox fields, includingmDefaultIVSize, from attacker-supplied MP4 bytes. - `ClearKeyDecryptor::Decrypt`
- the Clear Key GMP/CDM function that pads a sample’s IV to the AES block length before decrypting.
Root Cause Analysis
SinfParser::ParseTenc read the tenc default per-sample IV size directly from the MP4 byte stream into mSinf.mDefaultIVSize (an uint8_t) with no validation, so a crafted file could specify any value from 0 to 255. That size flowed into ClearKeyDecryptor::Decrypt, which pads the IV to the AES block size via iv.insert(iv.end(), CENC_KEY_LEN - aMetadata.mIV.size(), 0). The invariant that the IV never exceeds CENC_KEY_LEN (16) was assumed but never enforced outside a debug assert, so when aMetadata.mIV.size() exceeded CENC_KEY_LEN the unsigned subtraction CENC_KEY_LEN - aMetadata.mIV.size() wrapped around to an enormous count, driving a pathological std::vector insertion.
The fix enforces the invariant at both ends: ParseTenc now rejects any defaultIVSize other than 0, 8, or 16 with NS_ERROR_FAILURE, and Decrypt returns Status::kDecryptError when mIV.size() > CENC_KEY_LEN. Because release builds do not evaluate the assert, the explicit runtime checks are what actually close the boundary hole.
tenc box and then computing an unsigned padding count CENC_KEY_LEN - size without first bounding size; the fix adds explicit whitelist validation at parse time and a size guard at decrypt time so the subtraction can never underflow.Attack Path
- Craft malicious MP4
An attacker authors an encrypted MP4 whose
tencbox declares a default per-sample IV size larger than 16 (or otherwise not0/8/16). - Deliver to victim The file is served to a browser using the Clear Key EME/GMP path, e.g. via a web page or media element.
- Bypass parse validation
SinfParser::ParseTencstores the oversizedmDefaultIVSizeunchecked, propagating it to per-sample decryption metadata. - Trigger underflow
ClearKeyDecryptor::DecryptcomputesCENC_KEY_LEN - aMetadata.mIV.size(), which underflows to a huge unsigned count passed toiv.insert. - Corrupt decrypt state The pathological insertion produces a malformed or oversized IV buffer, yielding a decryption boundary error and abnormal behavior in the GMP process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/media/mp4/SinfParser.cpp |
modified |
Files Changed
dom/media/eme/clearkey/ClearKeyDecryptionManager.cppdom/media/mp4/SinfParser.cpp
Audit Directions
- Unsigned subtraction from parsed lengthsAudit every
CONST - sizeorsize - otherwheresizeoriginates from untrusted media metadata for wraparound before it feeds allocation orinsert/resizecounts. - Assert-only invariantsFlag places where a size or bounds invariant is guaranteed only by
assert, since release builds skip it; require an equivalent runtime check on any attacker-reachable path. - Container-format size fieldsTrace other MP4/box parsers (IV sizes, key-ID lengths, sample counts) to confirm each length is whitelisted or range-checked at parse time rather than trusted downstream in the GMP/CDM.
Patch
diff --git a/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp
index 328360a0311..eaea6bf09b6 100644
--- a/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp
+++ b/dom/media/eme/clearkey/ClearKeyDecryptionManager.cpp
@@ -265,6 +265,12 @@ Status ClearKeyDecryptor::Decrypt(uint8_t* aBuffer, uint32_t aBufferSize,
assert(aMetadata.mIV.size() == 8 || aMetadata.mIV.size() == 16 ||
(aMetadata.mIV.empty() && AllZero(aMetadata.mCipherBytes)));
+ if (aMetadata.mIV.size() > CENC_KEY_LEN) {
+ CK_LOGD("ClearKeyDecryptor::Decrypt unexpected IV size %zu",
+ aMetadata.mIV.size());
+ return Status::kDecryptError;
+ }
+
std::vector<uint8_t> iv(aMetadata.mIV);
iv.insert(iv.end(), CENC_KEY_LEN - aMetadata.mIV.size(), 0);
diff --git a/dom/media/mp4/SinfParser.cpp b/dom/media/mp4/SinfParser.cpp
index 4ffb4ff2f09..1455e89c0c5 100644
--- a/dom/media/mp4/SinfParser.cpp
+++ b/dom/media/mp4/SinfParser.cpp
@@ -7,6 +7,8 @@
#include "AtomType.h"
#include "Box.h"
#include "ByteStream.h"
+#include "MediaDataDemuxer.h"
+#include "mozilla/Logging.h"
#include "mozilla/Try.h"
namespace mozilla {
@@ -73,7 +75,14 @@ Result<Ok, nsresult> SinfParser::ParseTenc(const Box& aBox) {
}
uint8_t isEncrypted = MOZ_TRY(reader->ReadU8());
- mSinf.mDefaultIVSize = MOZ_TRY(reader->ReadU8());
+ uint8_t defaultIVSize = MOZ_TRY(reader->ReadU8());
+ if (defaultIVSize != 0 && defaultIVSize != 8 && defaultIVSize != 16) {
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Warning,
+ ("SinfParser: unexpected default per-sample IV size %u",
+ static_cast<unsigned>(defaultIVSize)));
+ return Err(NS_ERROR_FAILURE);
+ }
+ mSinf.mDefaultIVSize = defaultIVSize;
memcpy(mSinf.mDefaultKeyID, reader->Read(16), 16);
if (isEncrypted && mSinf.mDefaultIVSize == 0) {