Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactCryptographic Flaw in PDFium
DescriptionCryptographic Flaw in PDFium
ComponentPDFium
Bug ClassLogic Error
Tracker486906037
Fix commit9c97da0e4421 (pdfium) +3/-3
CISA KEVNot listed
Creditedmlafon
Disclosed2026-04-07

Changed Functions

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

Files Changed

  • core/fpdfapi/parser/cpdf_crypto_handler.cpp
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