CVE-2026-5889
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcore/fpdfapi/parser/cpdf_crypto_handler.cpp |
modified |
Files Changed
core/fpdfapi/parser/cpdf_crypto_handler.cpp
Patch
From 9c97da0e4421342fcf9406e76930a4cd8fb3eb67 Mon Sep 17 00:00:00 2001 From: Tom Sepez <[email protected]> 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 <[email protected]> Commit-Queue: Tom Sepez <[email protected]> --- 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.