SevyDevy

Guide · Backend

MuPDF's October 2026 PDF Encryption Flaw Explained: A 9-Bit AES Key

15 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The PDF encryption vulnerability that researchers at V12 published on 27 October 2026 is about weak randomness, not weak AES. MuPDF, a widely used open-source PDF library, built AES-256 file keys from the clock. The risk is in files it encrypted.
  • A file key is the random 32-byte secret that actually locks the pages. Your password only protects a copy of it, so a guessable file key opens the file without the password.
  • In MuPDF before 1.28.5, only 9 bits of the clock decided the key in a newly started program. That leaves 512 possible keys, which V12's post says take about 11 milliseconds to search.
  • MuPDF 1.28.5, released 25 September 2026, switched to a cryptographic random generator. In Python, PyMuPDF 2.0 is the first PyPI release built on it, as of 30 October 2026.
  • Beginners can stop after the fix section. Engineers get our tests: two programs started in the same second wrote identical encryption fields, and re-saving with the keep option kept the weak key.

What goes wrong when a strong cipher gets a guessable key?

The lock stays strong, but anyone can guess the key, so the encryption protects nothing. That is the PDF encryption vulnerability researchers at V12 published on 27 October 2026. It affects AES-256 PDFs written by MuPDF before version 1.28.5; we tested 1.28.2.

Picture a common setup. An HR service emails monthly payslips as password-protected PDFs, made by a Python script that uses PyMuPDF, the Python wrapper around MuPDF. The setup is made up, but the weakness is real and we reproduced it.

The team chose AES-256, the strongest option the PDF format offers. They used a long password per employee. None of that helped, because the secret that really locks each file came from the server clock.

Which words do you need before reading about PDF encryption?

  • AES-256: a widely trusted cipher, a method that scrambles data with a 256-bit key. No practical attack on it is known, so attacks go after how the key was made.
  • File key: the 32 random bytes that actually encrypt the pages of the PDF.
  • Password wrapping: your password is hashed slowly into a second key, which locks a copy of the file key inside the file.
  • Random number generator: code that produces unpredictable-looking numbers. A cryptographic one starts from the operating system's secret randomness, so it cannot practically be predicted.
  • Seed: the starting value of a generator. Same seed, same output, every time.

What is the PDF encryption vulnerability found in October 2026?

It is that several PDF tools made the random file key from predictable inputs, mainly the current time. V12's post, titled after the 9-bit AES key, covers MuPDF, Foxit PDF Reader, OpenPDF and iText.

For MuPDF, the post shows the generator was seeded once with the current time in seconds. The same stream then produced the document ID and the key. The post says only nine bits of the timestamp affect the actual AES-256 key.

Nine bits give 2 to the power 9, which is 512 starting points. The post says timestamps 512 seconds apart produce exactly the same byte stream. We confirmed both effects on PyMuPDF 1.28.2, shown later.

What V12's post says about each tool (read 30 October 2026). Only the MuPDF row was tested by us.
Tool and version named in the postHow the file key was madeFix stated in the post
MuPDF before 1.28.5Simple formula generator (drand48 style, explained below) seeded with the time in seconds; 9 bits matterYes; affects versions before 1.28.5
Foxit PDF Reader 2026.1.3.36551Hash of a timestamp, 32 uninitialised bytes and a constantNot stated
OpenPDF 3.0.5 and iText 9.7.1ARC4, an old stream cipher, seeded with time in milliseconds and free memoryNot stated

Uninitialised bytes are memory the program never set, so they hold leftovers rather than fresh randomness. Free memory is the amount of unused RAM, a number an attacker can often narrow down. Weak inputs give weak keys, whatever cipher comes after.

How does AES-256 encryption in a PDF actually work?

A PDF never encrypts pages with your password directly. It encrypts them with a random file key, then stores that key in a locked form that only your password can open.

Think of a hotel safe. The safe has a random factory combination, the file key. Your chosen PIN unlocks a small box holding that combination, the wrapped key.

Where the picture breaks: a hotel safe locks you out after a few wrong tries. A PDF never does. Whoever holds the file can try keys offline, as fast as their computer allows, so a short list of possible keys is fatal.

V12's post explains why the password side was fine. The current AES-256 scheme, revision 6, makes the password hashing deliberately slow, so guessing a good password is expensive. An attacker simply skips the password and guesses the file key instead.

How MuPDF before 1.28.5 made an AES-256 PDF

Notice that the clock feeds the file key and the document ID, and the ID sits in plain view. The password only touches the wrapping step.

Diagram connections:

  • Clock in seconds to drand48-style stream (Seeds once)
  • drand48-style stream to Document ID
  • drand48-style stream to 32-byte file key
  • 32-byte file key to Encrypted pages (Encrypts)
  • Your password to Wrapped key in the file (Hash locks the key)
  • 32-byte file key to Wrapped key in the file
  • Clock in seconds: Seeds the generator once per process
  • drand48-style stream: Only 9 clock bits reach the bytes used
  • Document ID: Drawn first, stored in plain view
  • 32-byte file key: Encrypts every page with AES-256
  • Your password: Slowly hashed with a salt
  • Wrapped key in the file: File key locked by the password hash
  • Encrypted pages: Safe only if the file key is unguessable

Why does a clock-seeded random generator break encryption?

Because a generator is just a formula. If an attacker can list every possible seed, they can rerun the formula for each one and get every possible key.

The next three paragraphs show the bit maths behind the number 512. If you only need the fix, skip to the fix section; you will not miss a step.

A drand48-style generator keeps a 48-bit state, a number it updates on every call by multiplying and adding a constant. V12's post says MuPDF set the low 16 bits to the fixed value 0x330e and put the timestamp above them.

In multiplication and addition, carries only move upwards, so higher bits never change lower bits. The standard drand48 functions return the top bits. Per the post, MuPDF instead took each key byte from bits 17 to 24.

So only state bits 0 to 24 matter. The fixed 0x330e fills bits 0 to 15, leaving the lowest nine timestamp bits. That is why a clock reading 512 seconds later repeats everything.

Our NIST curve seeds guide met the same generator family, where a 1997 timestamp explained a curve constant. There it was harmless. Here it guards secrets, which is where predictable seeds hurt.

Why could anyone guess the key without the password?

Because the file key never depended on the password. An attacker who has the PDF can generate each candidate key and check it, never touching the slow password step.

V12's post walks through one example file: with the clock value modulo 512 equal to 80, the recovered key decrypts the document without either password. The post also published decryptors, which we did not run.

The post says MuPDF drew the document ID from the stream and then immediately drew the key. The ID is stored unencrypted. That gives anyone a known piece of the stream to match candidates against.

In a newly started program that encrypts one file and exits, the 512-candidate figure applies directly. A payslip job that writes many files in one run is different: later files come from further along the same stream.

Do not treat batch-made files as safer. The readable ID sits right before each key in the stream, so it likely helps an attacker find the position too. We did not measure this.

How do you fix the MuPDF weak key problem in your app?

Upgrade to MuPDF 1.28.5 or later, then re-encrypt any file you still control with the fixed version. Copies already sent to people cannot be fixed afterwards.

  1. Find your MuPDF version

    In Python, run python -c "import pymupdf; print(pymupdf.mupdf_version)". A value below 1.28.5 is affected. If that import fails, your PyMuPDF is older than the releases tested here, so upgrade it. For other bindings, check the bundled MuPDF version in their release notes.

  2. Upgrade

    As of 30 October 2026, PyMuPDF 2.0 is the first PyPI release built on MuPDF 1.28.5. It needs Python 3.11 or newer; 1.28.2 allowed 3.10. Read its changelog first.

  3. Re-encrypt with a fresh key

    Open each stored file, call authenticate with its password, then save to a new path with encryption=pymupdf.PDF_ENCRYPT_AES_256 and both passwords again.

  4. Avoid the keep option for this

    PDF_ENCRYPT_KEEP preserves the existing encryption fields. In our test on 2.0, all five fields stayed identical, so the weak key survived.

  5. Verify

    Run the version command from step 1 again and expect 1.28.5 or later. Engineers can also run the same-second test below; every field should print different.

  6. Treat sent files as exposed

    Anyone holding an old copy may be able to open it. Decide with your security team whether the contents need action, such as notifying people.

For the payslip service, that means upgrading the generator script, re-saving the archive with fresh keys and logging which months went out on the old version. Those older payslips should be treated as readable by whoever holds them.

Checking and fixing an app that writes encrypted PDFs

Notice the second question: upgrading alone does not protect files made before the upgrade.

Diagram connections:

  • App writes encrypted PDFs to MuPDF 1.28.5 or later?
  • MuPDF 1.28.5 or later? to Upgrade the library (No)
  • MuPDF 1.28.5 or later? to Old encrypted files? (Yes)
  • Upgrade the library to Old encrypted files?
  • Old encrypted files? to Re-encrypt stored files (Stored copies)
  • Old encrypted files? to Treat sent copies as open (Sent copies)
  • Re-encrypt stored files to Same-second test
  • App writes encrypted PDFs: For example a payslip job
  • MuPDF 1.28.5 or later?: Check mupdf_version
  • Upgrade the library: PyMuPDF 2.0 bundles 1.28.5
  • Old encrypted files?: Stored or already sent
  • Re-encrypt stored files: AES-256 again, never the keep option
  • Treat sent copies as open: They cannot be changed now
  • Same-second test: Every field should differ

Beginners can stop here. You now know what broke, how to check your version and how to fix your files. The rest is for engineers: our tests, safer random generators and the interview answer.

How did we confirm the weak keys on PyMuPDF 1.28.2?

We encrypted the same one-page PDF in two separate Python processes started in the same second, then compared the encryption fields. No key was recovered and no one else's file was touched.

The PDF keeps its encryption details in named fields. /U and /O hold password check values with their salts, random bytes kept in plain view. /UE and /OE hold the wrapped file key. /Perms holds encrypted permission flags.

python
# one.py: write one AES-256 encrypted PDF, then exit
# Run twice in the same second with different file names
import sys
import time
import pymupdf

doc = pymupdf.open()
doc.new_page().insert_text((72, 72), "hello")
doc.save(
    sys.argv[1],
    encryption=pymupdf.PDF_ENCRYPT_AES_256,
    owner_pw="owner-pass",
    user_pw="user-pass",
)
print(sys.argv[1], int(time.time()))  # the second it finished
python
# cmp.py: compare the encryption fields of two PDFs
import re
import sys
import pymupdf


def fields(path):
    enc = pymupdf.open(path).xref_get_key(-1, "Encrypt")[1]
    out = {}
    for k in ("U", "O", "UE", "OE", "Perms"):
        m = re.search("/" + k + r"(<[0-9A-Fa-f]+>|\((?:\\.|[^\\)])*\))", enc)
        if not m:
            sys.exit("field " + k + " not found in " + path)
        out[k] = m.group(1)
    return out


a, b = fields(sys.argv[1]), fields(sys.argv[2])
for name in a:
    print(name, "identical" if a[name] == b[name] else "different")

On Linux or macOS, start both runs in one terminal line: python one.py a.pdf & python one.py b.pdf. Each run prints the second it started; if the numbers differ, run the line again.

Then run python cmp.py a.pdf b.pdf. On Windows, Command Prompt runs the two commands one after the other, so use WSL or Git Bash.

Our results on 30 October 2026, Linux, Python 3.13. Same-second counts are runs where both processes landed in the same second.
TestPyMuPDF 1.28.2 (MuPDF 1.28.2)PyMuPDF 2.0 (MuPDF 1.28.5)
Two processes, same second: /U, /O, /UE, /OE, /PermsAll five identical in 8 of 8All five different in 8 of 8
Two processes, same second: document IDIdentical in 8 of 8Different in 8 of 8
Faked clock 512 seconds apart, three start timesAll five identical in 3 of 3All five different in 3 of 3
Faked clock 1 or 256 seconds apartAll five differentAll five different
Two saves inside one processDifferentDifferent

What just happened? On 1.28.2, two separate programs produced the same salts and the same wrapped key. Identical /UE with identical salt and password means identical file keys. On 2.0, every field differed, which is what fresh randomness looks like.

The 512-second row used libfaketime, a tool that fakes the clock for one program. The first four rows came from one clean test script run on 30 October 2026; the last row came from an earlier single-process test.

We also checked the library files. The 1.28.2 build exports fz_srand48 and fz_drand48. The 1.28.5 build drops them and calls getentropy, the operating system's source of secure random bytes.

One more detail from our test: a file re-encrypted on 2.0 kept the first half of its old document ID, while all five key fields changed.

Which random generator should code use for keys and tokens?

Use the cryptographic generator your platform provides, never a general-purpose one seeded from time. The MuPDF fix moved in exactly that direction.

In Python, that means the secrets module or os.urandom, never the random module. Python's own documentation says the random module should not be used for security purposes.

In C on Linux and macOS, getentropy fills a buffer from the operating system. In Node.js, crypto.randomBytes does the same job. Each reads from a source the operating system keeps unpredictable.

A useful review question is: where does this secret's randomness come from? If the answer mentions time, a process ID or a counter, treat it as a finding.

How do you explain weak PDF encryption keys in a system design interview?

Frame it as a key management failure, not a cryptography failure. Interviewers want to hear that you separate the cipher, the key and where the key's randomness comes from.

  1. Name the layers

    Pages are encrypted with a random file key; the password only wraps that key.

  2. Find the weak link

    The file key came from a clock-seeded generator, leaving 512 candidates in a fresh process.

  3. Explain the impact

    Anyone with the file can try every candidate offline and skip the password entirely.

  4. Give the fix

    Use an operating-system random source, as MuPDF 1.28.5 does, and re-encrypt stored files.

  5. Add the lesson

    Secrets already shared stay exposed, so design for rotation and an inventory of what was sent.

If asked how you would catch this earlier, mention a cheap property test. Generate two keys in two fresh processes at the same moment and assert they differ.

Questions people ask

Is AES-256 broken by this PDF encryption vulnerability?
No. AES-256 itself is fine; the problem was how some tools made the key. In MuPDF before 1.28.5, the AES-256 file key came from a generator seeded with the clock. Guessing among a few hundred keys is easy, whatever cipher uses them.
Which MuPDF version fixes the weak PDF encryption keys?
MuPDF 1.28.5, released on 25 September 2026, switched to a ChaCha20 cryptographic random generator, per its release notes. V12 says versions before 1.28.5 are affected and that MuPDF fixed it. In Python, PyMuPDF 2.0 is the first PyPI release on MuPDF 1.28.5, as of 30 October 2026.
Does a long password protect a PDF made by an affected version?
No. The password only locks a copy of the file key. If the file key itself can be guessed, the attacker decrypts the pages directly and never needs your password. Slow password hashing protects the password, not a weak file key.
Do I need to re-encrypt PDFs made before the upgrade?
Yes, for files you still store. Upgrading only changes new files. Open each file with its password and save it again with AES-256 encryption chosen explicitly. Our test showed the keep option preserved the old encryption fields unchanged.
Are Foxit, OpenPDF and iText affected too?
V12's post describes weak key generation in Foxit PDF Reader 2026.1.3.36551, OpenPDF 3.0.5 and iText 9.7.1. It states a fix only for MuPDF. We did not test these tools, so check each vendor's advisory, as of 30 October 2026.
Is there a CVE for the MuPDF weak key bug?
We found none as of 30 October 2026. V12's post lists no CVE, and the MuPDF 1.28.5 notes identify this fix only as Bug 709668. PyMuPDF 2.0 also fixes CVE-2026-82035, a separate font-extraction bug, so do not confuse the two.

Sources and further reading

Practise this with Lala

Practise