Skip to content

fix: prevent /P permissions overflow from corrupting decryption key - #1632

Open
garcia-d wants to merge 2 commits into
LibrePDF:2.4from
garcia-d:fix/2.4/pdfnumber-int-overflow
Open

garcia-d wants to merge 2 commits into
LibrePDF:2.4from
garcia-d:fix/2.4/pdfnumber-int-overflow

Conversation

@garcia-d

@garcia-d garcia-d commented Sep 14, 2026

Copy link
Copy Markdown

Description of the new Feature/Bugfix

PdfNumber#intValue() narrowed the underlying double with a plain
(int) cast. Some PDF producers write the /Encrypt dictionary's /P
(permissions) entry using its unsigned 32-bit decimal representation
instead of the equivalent signed value (e.g. 4294965956 instead of
-1340) - both encode the identical 4-byte value, and this is spec-legal.

Java's narrowing double-to-int conversion saturates rather than wraps for
an out-of-range value, so such a /P value was misread as
Integer.MAX_VALUE instead of -1340. That corrupted the encryption key
the standard security handler derives, so its own recomputed /U check
failed for every password - including a correct, empty one - and
PdfReader reported BadPasswordException for a document that was never
actually password-protected.

Routed the cast through long first, which reproduces the expected
low-order-32-bit truncation instead of clamping. Applied identically to
both the legacy (com.lowagie) and modern (org.openpdf) copies of
PdfNumber this branch ships.

Related Issue: none (found while investigating a user-reported PDF, not
tied to a filed issue)

Unit-Tests for the new Feature/Bugfix

  • Unit-Tests added to reproduce the bug
  • Unit-Tests added to the added feature

Compatibilities Issues

No method signatures changed. Any caller previously relying on the
(incorrect) saturated value of an out-of-range PdfNumber#intValue() would
now see the correctly truncated value instead - this is a correctness fix,
not expected to be a compatibility concern in practice.

Your real name

Diego Garcia

Testing details

Added PdfNumberTest (direct truncation coverage) and
DecryptUnsignedPermissionsTest (a PdfReader-level regression test using
a crafted PDF whose /P is written this way with an empty user password),
to both modules. Full test suite run locally, no regressions (only 3
pre-existing, unrelated failures from a missing OS/2 table in the local
macOS Courier.ttc system font).

🤖 Generated with Claude Code

PdfNumber#intValue() narrowed the underlying double with a plain (int)
cast. Some PDF producers write the /Encrypt dictionary's /P (permissions)
entry using its unsigned 32-bit decimal representation instead of the
equivalent signed value (e.g. 4294965956 instead of -1340) - both encode
the identical 4-byte value, and this is spec-legal.

Java's narrowing double-to-int conversion saturates rather than wraps for
an out-of-range value, so such a /P value was misread as
Integer.MAX_VALUE instead of -1340. That corrupted the encryption key the
standard security handler derives, so its own recomputed /U check failed
for every password - including a correct, empty one - and PdfReader
reported BadPasswordException for a document that was never actually
password-protected.

Route the cast through long first, which reproduces the expected
low-order-32-bit truncation instead of clamping. Applied identically to
both the legacy (com.lowagie) and modern (org.openpdf) copies of
PdfNumber, since this branch ships both.

Add PdfNumberTest covering the truncation directly, and
DecryptUnsignedPermissionsTest, a PdfReader-level regression test using a
crafted PDF whose /P is written this way with an empty user password, to
both modules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

DecryptUnsignedPermissionsTest reached PdfReader's private
ownerPasswordUsed field via reflection (setAccessible), which Codacy
flagged as a high-severity visibility-alteration issue, plus a medium
issue for wrapping the reflection exceptions in a raw RuntimeException.

PdfReader already exposes a public isOwnerPasswordUsed() getter that
returns the same field, so the reflection is unnecessary. Calling it
directly returns the identical value and removes both findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@garcia-d
garcia-d force-pushed the fix/2.4/pdfnumber-int-overflow branch from e4c66a5 to 94f3a8f Compare September 14, 2026 22:17
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
12.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@garcia-d

Copy link
Copy Markdown
Author

Hi @asturio,

This PR backports the fix preventing /P permissions overflow from corrupting the decryption key to the 2.4 branch. The same fix has been opened for every active branch: #1627 (1.3-java8), #1628 (1.4-java11), #1629 (2.0-java17), #1630 (2.2-java21), #1631 (master).

Heads up: the SonarCloud duplication gate is red here, but it's not a code-quality issue — this PR touches the legacy/modern mirror modules, which are near-identical by design, so the shared fix logic gets flagged as "duplicated" between them. There are zero actual Sonar issues (bugs/vulnerabilities/smells) on this PR.

Whenever you get a chance, I'd appreciate your review, and if it looks good, approval/merge into 2.4.

I'd also kindly ask, once merged, if an official release including this fix could be cut — particularly for the Java 8-compatible line (#1627), since I'd rather rely on an official release than patch it in locally.

Thanks for maintaining OpenPDF!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant