Medium watch CVSS 4.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactCryptographic Flaw in PDFium
DescriptionCryptographic Flaw in PDFium
ComponentPDFium
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker486906037
Fix commit9c97da0e4421 (pdfium) +3/-3
CWECWE-326
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
Creditedmlafon
Reported2026-02-23 (per vendor advisory)
Fix landed2026-02-24 (public git)
Disclosed2026-04-07
Patch gap42 days — fix public in source before the advisory

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_t words.

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.

Key insight
The single mistake was sourcing a security-critical AES-CBC IV from the predictable rand() PRNG; the fix swaps it for the cryptographically stronger FX_Random::Fill.

Attack Path

  1. Obtain attacker-influenced ciphertext An attacker collects PDF content streams that PDFium encrypted via EncryptContent, each carrying its rand()-derived IV in the first kIVSize bytes.
  2. 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).
  3. 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

The vendor advisory rates this medium, “Cryptographic Flaw in PDFium,” with no exploited-in-the-wild claim. The weakness is a reduced-strength encryption: PDFs encrypted by PDFium’s 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

FunctionChangeNotes
for
core/fpdfapi/parser/cpdf_crypto_handler.cpp
modified

Files Changed

  • core/fpdfapi/parser/cpdf_crypto_handler.cpp

Audit Directions

  • Non-crypto PRNG in crypto contexts
    Grep for rand(), srand, and similar standard-library PRNGs used to produce IVs, nonces, salts, or keys, and replace them with FX_Random/a CSPRNG.
  • IV/nonce uniqueness and entropy
    Audit every AES-CBC/CTR/GCM call site to confirm the IV/nonce is both unpredictable and non-repeating per key.
  • Span reinterpretation around randomness
    Review uses of reinterpret_span/FromOwningPointer feeding crypto fills to ensure the byte length and element width match so the whole IV is actually randomized.
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));
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. 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 bans std:: random engines (with guidance to use base/rand_util.h), but the C rand() in PDFium was not caught by this check.

  2. srand() is never called. A codebase-wide search across all .cpp, .c, and .h files confirms zero occurrences of srand in the entire PDFium repository. Chromium’s own process initialization (content/app/content_main_runner_impl.cc) does not call srand() either. Per the C standard (C11 §7.22.2.2): “If rand is called before any calls to srand have been made, the same sequence shall be generated as when srand is first called with a seed value of 1.” 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:

  1. The PDF uses AES encryption (cipher_ == Cipher::kAES), i.e., encryption revision ≥ 4 — the modern and recommended encryption mode.
  2. The document is saved via FPDF_SaveAsCopy() or FPDF_SaveWithVersion().
  3. The FPDF_REMOVE_SECURITY flag is not set (this is the default behavior).
  4. The CPDF_Creator constructor preserves the security handler from the loaded document (cpdf_creator.cpp:136-137), so GetCryptoHandler() 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.cpp contains zero #ifdef directives.
  • The build_with_chromium flag 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

  1. core/fpdfapi/parser/cpdf_crypto_handler.cpp, line 99 calls rand() to generate the AES IV.
  2. A search for srand across all .cpp, .c, and .h files in the PDFium repository returns zero results.
  3. A search for srand in Chromium’s process initialization code (content/app/content_main_runner_impl.cc) also returns zero results.
  4. Per C11 §7.22.2.2, rand() without a prior srand() call always produces the same sequence as srand(1).
  5. 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:

  1. Open text_form_encrypted_aes.pdf in Chrome, enter password user123.
  2. Type text into the form field, save (Ctrl+S) → saved1.pdf.
  3. Close the browser. Re-open Chrome and the same PDF, enter password, type different text, save → saved2.pdf.
  4. 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 of cpdf_crypto_handler.cpp.
  • Consider adding rand / srand to PDFium’s PRESUBMIT.py banned function list to prevent future occurrences.
View on issue tracker