CVE-2026-5889
Overview
Background
- PDFium
- Chrome’s bundled PDF rendering/parsing library, which implements the encryption used by password- and permissions-protected PDFs.
- AES-CBC IV
- The initialization vector prepended to AES-CBC ciphertext; CBC security requires each IV to be unpredictable/unique, otherwise identical plaintexts leak equality and the cipher becomes vulnerable.
- `rand()`
- The C standard-library pseudo-random generator, which is not cryptographically secure and typically has a small, predictable state seeded weakly.
- `FX_Random::Fill`
- PDFium’s cryptographically-stronger random fill routine, operating over a span of
uint32_twords.
Root Cause Analysis
The AES-CBC encryption path in cpdf_crypto_handler.cpp generated the per-message IV by looping over dest_iv_span and filling each byte with static_cast<uint8_t>(rand()). Because rand() is a non-cryptographic PRNG with limited, often predictably-seeded state, the IV it produced was weak/predictable rather than unpredictable as AES-CBC requires, violating the “IV must be unpredictable and unique” invariant for CBC mode.
The fix replaces that loop with FX_Random::Fill(fxcrt::reinterpret_span<uint32_t>(dest_iv_span)), drawing the IV from PDFium’s stronger random source, and adds the fx_random.h and span_util.h includes to support it. The IV is still written into dest_iv_span and passed to CRYPT_AESSetIV before CRYPT_AESEncrypt, so only the randomness source changed, not the control flow; this is a cryptographic-quality fix, not a bounds/validation change.
rand() PRNG; the fix swaps it for the cryptographically stronger FX_Random::Fill.Attack Path
- Obtain attacker-influenced ciphertext
An attacker collects PDF content streams that PDFium encrypted via
EncryptContent, each carrying itsrand()-derived IV in the firstkIVSizebytes. - Predict or correlate the IV
Because the IV comes from
rand(), its values are predictable or low-entropy, undermining CBC’s requirement for unpredictable IVs (exact recoverable information depends on usage) (inferred). - Exploit the weak IV With predictable IVs, standard CBC weaknesses (e.g. plaintext-equality leakage or chosen-plaintext distinguishing) become feasible against the produced ciphertext (inferred); the patch itself only establishes that the IV was weak, not a concrete decryption.
Impact Assessment
EncryptContent used predictable AES-CBC IVs, weakening confidentiality guarantees for anyone relying on that encryption; any concrete plaintext recovery is a downstream cryptographic consequence not demonstrated by the diff (inferred). It is a logic/crypto-quality flaw with no memory-safety or escalation component shown.Changed Functions
| Function | Change | Notes |
|---|---|---|
forcore/fpdfapi/parser/cpdf_crypto_handler.cpp |
modified |
Files Changed
core/fpdfapi/parser/cpdf_crypto_handler.cpp
Audit Directions
- Non-crypto PRNG in crypto contextsGrep for
rand(),srand, and similar standard-library PRNGs used to produce IVs, nonces, salts, or keys, and replace them withFX_Random/a CSPRNG. - IV/nonce uniqueness and entropyAudit every AES-CBC/CTR/GCM call site to confirm the IV/nonce is both unpredictable and non-repeating per key.
- Span reinterpretation around randomnessReview uses of
reinterpret_span/FromOwningPointerfeeding crypto fills to ensure the byte length and element width match so the whole IV is actually randomized.
Patch
From 9c97da0e4421342fcf9406e76930a4cd8fb3eb67 Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Tue, 24 Feb 2026 15:23:10 -0800
Subject: [PATCH] Use better IV in PDF_CryptoHandler::EncryptContent().
Bug: 486906037
Change-Id: I58dd3d70371ef309adc3da1d55a936606205e202
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/143691
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
---
diff --git a/core/fpdfapi/parser/cpdf_crypto_handler.cpp b/core/fpdfapi/parser/cpdf_crypto_handler.cpp
index 2b91d5f..35e298c 100644
--- a/core/fpdfapi/parser/cpdf_crypto_handler.cpp
+++ b/core/fpdfapi/parser/cpdf_crypto_handler.cpp
@@ -26,6 +26,8 @@
#include "core/fpdfapi/parser/cpdf_string.h"
#include "core/fxcrt/check.h"
#include "core/fxcrt/check_op.h"
+#include "core/fxcrt/fx_random.h"
+#include "core/fxcrt/span_util.h"
#include "core/fxcrt/stl_util.h"
namespace {
@@ -95,9 +97,7 @@
auto dest_data_span = dest_span.subspan(kIVSize, source_data_size);
auto dest_padding_span = dest_span.subspan(kIVSize + source_data_size);
- for (auto& v : dest_iv_span) {
- v = static_cast<uint8_t>(rand());
- }
+ FX_Random::Fill(fxcrt::reinterpret_span<uint32_t>(dest_iv_span));
CRYPT_AESSetIV(aes_context_.get(), dest_iv_span);
CRYPT_AESEncrypt(aes_context_.get(), dest_data_span,
source.first(source_data_size));
Original Bug Report
PDFium: AES-CBC Initialization Vectors generated using unseeded rand()
Summary
CPDF_CryptoHandler::EncryptContent() generates AES-CBC Initialization Vectors using the C standard library rand(). Neither PDFium nor Chromium ever call srand(), so rand() uses its default seed. The resulting IV sequence is deterministic and identical across every process invocation. This breaks the IND-CPA security of AES-CBC for every encrypted PDF saved through PDFium.
Affected component
- Component: Internals>Plugins>PDF
- File:
core/fpdfapi/parser/cpdf_crypto_handler.cpp, line 99 - Tested at: PDFium commit
541175f97(current HEAD of main) - Origin: Present since initial PDFium commit (
5110c4743), inherited from Foxit Software codebase - CWE: CWE-329: Generation of Predictable IV with CBC Mode
Vulnerable code
core/fpdfapi/parser/cpdf_crypto_handler.cpp, lines 87–101:
static constexpr size_t kIVSize = 16;
static constexpr size_t kPaddingSize = 16;
// ...
DataVector<uint8_t> dest(kIVSize + source_data_size + kPaddingSize);
auto dest_span = pdfium::span(dest);
auto dest_iv_span = dest_span.first<kIVSize>();
// ...
for (auto& v : dest_iv_span) {
v = static_cast<uint8_t>(rand()); // ← VULNERABLE
}
CRYPT_AESSetIV(aes_context_.get(), dest_iv_span);
CRYPT_AESEncrypt(aes_context_.get(), dest_data_span,
source.first(source_data_size));
Root cause
Two compounding issues:
-
rand()is not a CSPRNG. It is typically a linear congruential generator with ~32 bits of internal state, trivially predictable after observing a few output bytes. Chromium’s PRESUBMIT.py bansstd::random engines (with guidance to usebase/rand_util.h), but the Crand()in PDFium was not caught by this check. -
srand()is never called. A codebase-wide search across all.cpp,.c, and.hfiles confirms zero occurrences ofsrandin the entire PDFium repository. Chromium’s own process initialization (content/app/content_main_runner_impl.cc) does not callsrand()either. Per the C standard (C11 §7.22.2.2): “Ifrandis called before any calls tosrandhave been made, the same sequence shall be generated as whensrandis first called with a seed value of1.” The IV sequence is therefore fully deterministic and identical for every process, every user, every machine.
Contrast with existing PDFium infrastructure
PDFium already has FX_Random (core/fxcrt/fx_random.h), a Mersenne Twister implementation with platform-appropriate seeding (CryptGenRandom on Windows, time+PID+stack-address entropy on POSIX). It is used for file ID generation in cpdf_creator.cpp:114-117. This existing infrastructure was not used for IV generation.
The file also includes an unused #include <time.h> on line 9 — time() is never called anywhere in the file. This strongly suggests the original Foxit code may have intended to call srand(time(NULL)) but it was either removed or never added.
Reachability from Chromium
Call chain
User saves an encrypted PDF in Chrome (e.g. after filling a form)
→ FPDF_SaveAsCopy(document, writer, flags=0) [fpdfsdk/fpdf_save.cpp:210]
→ DoDocSave() [fpdf_save.cpp:174]
→ CPDF_Creator::Create() [cpdf_creator.cpp:613]
→ CPDF_Creator::WriteIndirectObj() [cpdf_creator.cpp:143]
→ if (GetCryptoHandler() && pObj != encrypt_dict_) [line 149]
→ CPDF_Encryptor(GetCryptoHandler(), objnum) [line 150]
→ CPDF_CryptoHandler::EncryptContent() [cpdf_crypto_handler.cpp:59]
→ rand() × 16 to fill AES IV [line 99]
Triggering conditions
The vulnerable code is reached when all of the following hold:
- The PDF uses AES encryption (
cipher_ == Cipher::kAES), i.e., encryption revision ≥ 4 — the modern and recommended encryption mode. - The document is saved via
FPDF_SaveAsCopy()orFPDF_SaveWithVersion(). - The
FPDF_REMOVE_SECURITYflag is not set (this is the default behavior). - The
CPDF_Creatorconstructor preserves the security handler from the loaded document (cpdf_creator.cpp:136-137), soGetCryptoHandler()returns non-null.
In Chromium, this scenario occurs when a user opens a password-protected AES-encrypted PDF, modifies it (e.g., fills in a form field), and saves.
No compile-time guards
cpdf_crypto_handler.cppcontains zero#ifdefdirectives.- The
build_with_chromiumflag does not gate any encryption code path. - The encryption sources are unconditionally compiled in
core/fpdfapi/parser/BUILD.gn.
Security impact
Deterministic IVs across all documents, all users, all machines
Since the rand() sequence is always seeded with 1, every saved encrypted PDF contains the same IV sequence. The first encrypted object always gets the same 16-byte IV, the second object always gets the next 16 bytes from the same deterministic sequence, and so on.
This means:
- Cross-document comparison: Comparing the ciphertext of object N between any two documents reveals whether they contain identical plaintext (same IV + same key + same plaintext = same ciphertext).
- Within-document analysis: Objects at the same encryption position always use the same IV, so identical streams/strings produce identical ciphertext, revealing structural information about the document.
AES-CBC security violation
AES-CBC requires that IVs be unpredictable (NIST SP 800-38A, §6.2). With deterministic IVs:
- The encryption scheme is not IND-CPA secure. An attacker can distinguish between encryptions of different plaintexts.
- Chosen-plaintext attacks become practical. If an attacker can influence any plaintext in the document (e.g., a form field value), they can XOR their known plaintext with the known IV and a target IV to recover other plaintext blocks sharing the same key.
- Block equality leaks. Two plaintext blocks that are identical always encrypt to identical ciphertext when using the same key and the same predictable IV.
Scope
This affects every PDFium embedder that saves AES-encrypted PDFs:
- Chromium (Chrome’s built-in PDF viewer, when saving modified encrypted PDFs)
- Android (system PDF rendering via PDFium)
- LibreOffice and any other third-party application using the PDFium library
Reproduction
Proof via source code analysis
core/fpdfapi/parser/cpdf_crypto_handler.cpp, line 99 callsrand()to generate the AES IV.- A search for
srandacross all.cpp,.c, and.hfiles in the PDFium repository returns zero results. - A search for
srandin Chromium’s process initialization code (content/app/content_main_runner_impl.cc) also returns zero results. - Per C11 §7.22.2.2,
rand()without a priorsrand()call always produces the same sequence assrand(1). - Therefore, every invocation of
EncryptContent()across every process produces the same deterministic IV sequence.
Proof via Chrome
Tested on Chrome 133 / Linux (Fedora 43, x86_64).
Setup: text_form.pdf from PDFium’s test resources (a single-page PDF with one editable text field) was encrypted with AES-256 (revision 6) using qpdf:
qpdf --encrypt user123 owner123 256 --modify=all --extract=y --annotate=y -- \
testing/resources/text_form.pdf text_form_encrypted_aes.pdf
Steps:
- Open
text_form_encrypted_aes.pdfin Chrome, enter passworduser123. - Type text into the form field, save (Ctrl+S) →
saved1.pdf. - Close the browser. Re-open Chrome and the same PDF, enter password, type different text, save →
saved2.pdf. - Repeat once more →
saved3.pdf.
Extract the IVs (the 16 bytes at the start of each encrypted stream):
python3 -c "
import re, sys
for fn in sys.argv[1:]:
with open(fn, 'rb') as f:
data = f.read()
print(f'=== {fn} ===')
for i, m in enumerate(re.finditer(b'stream\r?\n', data)):
start = m.end()
if start + 16 <= len(data):
print(f' stream {i}: IV = {data[start:start+16].hex()}')
" saved1.pdf saved2.pdf saved3.pdf
Result — all three files produce byte-identical IVs:
=== saved1.pdf ===
stream 0: IV = abb2cdc69bb454110e827441213ddc87
stream 2: IV = 70e93ea141e1fc673e017e97eadc6b96
stream 4: IV = 8f385c2aecb03bfb32af3c54ec18db5c
stream 6: IV = 05eff700e9a13ae5ca0bcbd0484764bd
stream 8: IV = 1f231ea81c7b64c514735ac55e4b7963
stream 10: IV = 3b706424119e09dcaad4acf21b10af3b
=== saved2.pdf ===
stream 0: IV = abb2cdc69bb454110e827441213ddc87
stream 2: IV = 70e93ea141e1fc673e017e97eadc6b96
stream 4: IV = 8f385c2aecb03bfb32af3c54ec18db5c
stream 6: IV = 05eff700e9a13ae5ca0bcbd0484764bd
stream 8: IV = 1f231ea81c7b64c514735ac55e4b7963
stream 10: IV = 3b706424119e09dcaad4acf21b10af3b
=== saved3.pdf ===
stream 0: IV = abb2cdc69bb454110e827441213ddc87
stream 2: IV = 70e93ea141e1fc673e017e97eadc6b96
stream 4: IV = 8f385c2aecb03bfb32af3c54ec18db5c
stream 6: IV = 05eff700e9a13ae5ca0bcbd0484764bd
stream 8: IV = 1f231ea81c7b64c514735ac55e4b7963
stream 10: IV = 3b706424119e09dcaad4acf21b10af3b
Three separate Chrome sessions, three separate save operations with different form content, yet every encrypted stream uses the exact same IV. This confirms that the AES IVs are fully deterministic in Chrome’s PDF save path.
Suggested fix
Replace rand() with a cryptographically secure random number generator:
// Option A: Platform CSPRNG (recommended)
#if BUILDFLAG(IS_WIN)
#include <bcrypt.h>
BCryptGenRandom(nullptr, dest_iv_span.data(),
static_cast<ULONG>(dest_iv_span.size()),
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
#elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA)
#include <sys/random.h>
getrandom(dest_iv_span.data(), dest_iv_span.size(), 0);
#endif
// Option B: base::RandBytes (when building within Chromium)
#include "base/rand_util.h"
base::RandBytes(dest_iv_span);
As a secondary cleanup:
- Remove the unused
#include <time.h>on line 9 ofcpdf_crypto_handler.cpp. - Consider adding
rand/srandto PDFium’sPRESUBMIT.pybanned function list to prevent future occurrences.