Files
pdf/docs/PDF_SECURITY_FEATURE_AUDIT.md
T
2026-08-13 19:05:14 +05:30

24 KiB

PDF Security / Unlock / Protect — Feature Audit Report

Date of Audit: August 13, 2026
Audited Subsystems: Frontend (pdf/frontend), Gateway API (pdf/gateway), C++ PDF Engine (pdf/engine), Pybind11 Bindings (pdf/bindings), Security Tests (pdf/tests/security)
Audit Purpose: Evaluate the exact current state of PDF security, password authentication, permission enforcement, encryption detection, password removal, and PDF protection capabilities in the existing codebase.


1. Executive Summary

The existing PDF editor codebase possesses a robust reading, authenticating, and permission-enforcing pipeline for password-protected PDFs, but lacks all writing/creation capabilities for PDF encryption and protection.

Key findings:

  • Reading & Authenticating Encrypted PDFs: FULLY IMPLEMENTED. The system detects encrypted PDFs, prompts the user via a React modal, passes the password to PDFium in C++, validates credentials, returns helpful error messages on invalid passwords, and renders the document upon success.
  • Granular Permission Surfacing & Enforcement: FULLY IMPLEMENTED. PDFium extracts security revision numbers, encryption types (RC4, AES-128, AES-256), and permission flags. The Python FastAPI Gateway exposes these flags in PermissionsResponse and enforces HTTP 403 Forbidden errors if a user attempts forbidden edits (annotations, text replacements, page rotations) or unauthorized exports.
  • Password Removal / Unlocking: PARTIALLY IMPLEMENTED (Implicit). Opening a protected PDF with a valid password loads the decrypted document into memory. Exporting the document via /documents/{id}/export writes out an unencrypted PDF file. However, there is no explicit UI button or API endpoint dedicated to "Remove Password".
  • Protecting / Encrypting PDFs: NOT IMPLEMENTED / MISSING. There is no functionality in the C++ engine (PDFium/QPDF), Pybind11 bindings, Gateway API, or Frontend UI to password-protect an unencrypted PDF, set user/owner passwords, or configure output permissions.

2. User-Facing Capability Summary

CURRENTLY AVAILABLE

  • ✓ Open Password-Protected PDFs: Prompts for credentials when an encrypted PDF is uploaded.
  • ✓ Password Validation & Error Feedback: Rejects incorrect passwords with clear inline UI feedback and allows unlimited retries.
  • ✓ Post-Authentication Rendering & Extraction: Full page rendering, OCR, text extraction, font listing, layout analysis, and display list extraction work seamlessly after authentication.
  • ✓ Encryption & Security Inspection: Detects and displays encryption standards (RC4-40, RC4-128, AES-128, AES-256) and security revision level (2 through 6) in the Inspector panel.
  • ✓ Permission Enforcement: Gateway blocks unauthorized edits, annotations, page reordering, and exports with HTTP 403 Forbidden responses if disallowed by the PDF's security settings.
  • ✓ Unprotected Export: Exporting an authenticated PDF generates an unencrypted PDF that can subsequently be opened without a password.

NOT CURRENTLY AVAILABLE

  • ✗ Explicit Password Removal UI/API: No button or endpoint explicitly labeled "Unlock PDF" or "Remove Security".
  • ✗ Password-Protect PDF / Lock PDF: Cannot apply passwords to an unencrypted PDF.
  • ✗ Configure Output Permissions: Cannot set or modify permission flags for printing, copying, editing, or annotating.
  • ✗ Separate Owner Password Prompting: Prompts only with a generic "Document password" input; does not request owner password specifically when attempting restricted operations.
  • ✗ Re-encrypting Edited PDFs: Saved/exported PDFs are saved without encryption.
  • ✗ Attempt Rate Limiting: No rate limiting on password validation attempts at the API level.

3. Protected PDF Open Flow

The upload and document initialization flow is traced across the full stack:

User Selects Encrypted PDF
           │
           ▼
[Frontend] gatewayService.uploadDocument(file, password="")
           │
           ▼ (POST /documents)
[Gateway API] upload_document() in crud.py
           │
           ▼
[Pybind11] PdfDocument.load_from_memory(bytes_data, "")
           │
           ▼
[C++ Engine] PdfDocument::loadFromMemory() -> FPDF_LoadMemDocument()
           │
           ▼ (PDFium returns FPDF_ERR_PASSWORD)
[C++ Engine] mapPdfiumError() returns EngineError::PasswordRequired
           │
           ▼
[Pybind11] Throws ValueError("Password required to open this PDF")
           │
           ▼
[Gateway API] Catches ValueError -> Raises HTTP 401 ("Password required")
           │
           ▼
[Frontend] gatewayService catches 401 -> Throws PasswordError
           │
           ▼
[Frontend] App.tsx sets passwordPrompt state -> PasswordModal renders

Exact Code Implementation Points

  • Encryption Detection & Password Loading (C++ Engine):
    • File: pdf/engine/src/parser/pdfium_document.cpp
    • Function: pdfengine::PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password)
    • C++ API: FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()), password.empty() ? nullptr : password.c_str())
  • Error Mapping (C++ Engine):
    • File: pdf/engine/src/parser/pdfium_internal.cpp
    • Function: pdfengine::parser::mapPdfiumError(unsigned long err, bool passwordProvided)
    • Logic: Maps FPDF_ERR_PASSWORD to EngineError::PasswordRequired (if password is empty) or EngineError::InvalidPassword (if password was provided).
  • Pybind11 Translation:
    • File: pdf/bindings/python/pdfengine_py.cpp
    • Function: throw_on_error(pdfengine::EngineError err)
    • Logic: Maps PasswordRequired -> PyExc_ValueError("Password required to open this PDF") and InvalidPassword -> PyExc_ValueError("Invalid password provided for this PDF").
  • Gateway Endpoint:
    • File: pdf/gateway/app/routers/documents/crud.py
    • Endpoint: POST /documents?password={password} (upload_document)
    • Logic: Catches ValueError from pybind11 and raises HTTPException(status_code=401, detail="Password required") or HTTPException(status_code=401, detail="Invalid password").
  • Frontend Password Dialog & Resubmission:

4. Password Authentication & Validation Flow

Stage Implementation Status Evidence / Location
Password Reaches Backend IMPLEMENTED uploadDocument(file, password) in gatewayService.ts:L476 sends POST /documents?password=....
Backend Passes Password to C++ IMPLEMENTED crud.py:L173 calls doc = pdfengine.PdfDocument.load_from_memory(bytes_data, password).
C++ Engine Validation IMPLEMENTED pdfium_document.cpp:L42 executes FPDF_LoadMemDocument(..., password.c_str()).
Incorrect Password Rejected IMPLEMENTED pdfium_internal.cpp:L280 returns EngineError::InvalidPassword -> HTTP 401 "Invalid password" -> App.tsx shows red error message in modal.
Correct Password Accepted IMPLEMENTED FPDF_LoadMemDocument returns document pointer -> Gateway stores document info and returns HTTP 201 response.
Password Retry Loop IMPLEMENTED App.tsx:L650 retains modal open on failure with updated error message, allowing infinite retry attempts.
Rate Limiting / Attempt Limit MISSING Neither Gateway nor C++ Engine tracks failed attempts or implements delays/lockouts.

5. Rendering & Feature Support After Authentication

Once authenticated, all engine features operate on the unlocked in-memory PDF handle:

Feature Status Evidence / Implementation Location
Page Count IMPLEMENTED pdfium_document.cpp:L101 (FPDF_GetPageCount)
Document Metadata IMPLEMENTED pdfium_document.cpp:L109 (FPDF_GetMetaText)
Font Inventory IMPLEMENTED pdfium_document.cpp:L280, fonts.py:L16
Page Text Extraction IMPLEMENTED pdfium_page.cpp:L100, render.py:L172
Page Image Rendering IMPLEMENTED pdfium_page.cpp:L50, render.py:L19
Display List Extraction IMPLEMENTED pdfium_page.cpp:L300, content.py:L133
OCR Support IMPLEMENTED ocr.py:L63 (Runs Tesseract on rendered page image)
Layout Model Extraction IMPLEMENTED layout.py:L120 (extract_document_model())
Editing Operations PARTIAL edits.py:L395 checks permissions. Allowed edits modify stream in-memory.
Exporting Document IMPLEMENTED export.py:L17 checks canCopy and calls save_full_for_export().

6. Unlock / Decrypt Capability

Action Status Description
Open Protected PDF IMPLEMENTED Via upload_document with password.
Authenticate IMPLEMENTED Validated via FPDF_LoadMemDocument.
In-Memory Decryption IMPLEMENTED PDFium decrypts document structure in RAM for standard operations.
Save/Export Unprotected Copy IMPLEMENTED (Implicit) doc.save_full_for_export() calls PDFium's FPDF_SaveWithVersion(doc_, &writer, 0, 14). Because PDFium does not attach an encryption handler during save, the output PDF is unencrypted.
Reopen Exported Copy Without Password IMPLEMENTED The exported PDF contains no /Encrypt dictionary; reopening requires no password.
Explicit "Remove Password" Endpoint / UI MISSING No dedicated route (e.g. POST /documents/{id}/unlock) or UI action exists.

7. Protect / Encrypt Capability

A comprehensive search across C++ Engine (pdf/engine), Pybind11 (pdf/bindings), Gateway (pdf/gateway), and Frontend (pdf/frontend) reveals NO code for creating encrypted PDFs:

  • QPDF Encryption Writer: Not implemented. qpdf_writer.cpp contains stream replacement and appearance helpers, but no QPDFWriter::setEncryption calls.
  • PDFium Encryption Output: PDFium's public writing API lacks native PDF encryption creation functions.
  • Python Encryption Libraries: pikepdf is imported only in pdf/tests/security/test_permissions.py to generate test fixtures. It is not present in Gateway production services.
  • Frontend Encryption Controls: No modal, form, or state exists for password-protecting documents.

8. Password Types & Permission Management

Password Types

  • User Password: IMPLEMENTED. Used for opening documents.
  • Owner Password: PARTIALLY IMPLEMENTED. When opened with an Owner password, PDFium elevates document permissions. The C++ engine detects this by comparing user vs doc permissions (perms.ownerUnlocked = (p != up) in pdfium_document.cpp:L168) and displays an "Owner" badge in InspectorPanel.tsx:L569. However, there is no UI workflow to enter an owner password separately to unlock restricted actions.

PDF Permissions Matrix

Permission Existing Support Where Implemented
Print (canPrint) Surfaced & Displayed pdfium_document.cpp:L171, store.py:L18, InspectorPanel.tsx:L573
Modify (canModify) Surfaced & Enforced pdfium_document.cpp:L172, edits.py:L375, edits.py:L395 (HTTP 403)
Copy (canCopy) Surfaced & Enforced pdfium_document.cpp:L173, export.py:L30, export.py:L65 (HTTP 403 on Export)
Extract (canCopy) Surfaced & Enforced Same bit as Copy (0x10) in PDFium spec
Annotate (canAnnotate) Surfaced & Enforced pdfium_document.cpp:L174, edits.py:L370-374, edits.py:L395 (HTTP 403)
Fill Forms (canFillForms) Surfaced & Enforced pdfium_document.cpp:L175, edits.py:L376, edits.py:L395
Accessibility (canExtractForAccessibility) Surfaced & Displayed pdfium_document.cpp:L176, store.py:L24
Document Assembly (canAssemble) Surfaced & Enforced pdfium_document.cpp:L177, edits.py:L377 (HTTP 403 on rotation/deletion)
High-Quality Print (canPrintHighRes) Surfaced & Displayed pdfium_document.cpp:L178, store.py:L19
Permission Configuration (Writing) MISSING No engine or gateway code exists to modify permission flags.

9. Encryption Algorithm Surfacing

The C++ engine inspects the PDF security handler revision via FPDF_GetSecurityHandlerRevision(doc_):

// pdfium_document.cpp (lines 147-164)
switch (rev) {
    case 2: perms.encryption = "RC4-40"; break;
    case 3: perms.encryption = "RC4-128"; break;
    case 4: perms.encryption = "AES-128"; break;
    case 5:
    case 6: perms.encryption = "AES-256"; break;
    default: perms.encryption = "Unknown"; break;
}
  • Surfacing: Mapped to PermissionsResponse.encryption (document.py:L12) and rendered as a badge in the Inspector panel (InspectorPanel.tsx:L567).
  • Creation: Encryption creation was not found in the codebase.

10. Frontend UI State

Capability Status Front-End Evidence
A. Unlock existing protected PDF IMPLEMENTED PasswordModal.tsx renders when passwordPrompt state is non-null.
B. Remove password / security MISSING No UI button or option.
C. Protect an unprotected PDF MISSING No UI button or option.
D. Set a password MISSING No input fields for protecting PDFs.
E. Configure permissions MISSING No permissions toggle matrix in settings or export dialog.
F. Export document IMPLEMENTED TopBar export button triggers file download.

11. Gateway / API Endpoints Audit

Endpoint Method Purpose Implemented Behavior C++ Call
/documents POST Upload & open PDF Accepts password query param. Passes password to engine. Returns HTTP 401 on missing/wrong password, HTTP 201 with permissions on success. PdfDocument::loadFromMemory
/documents/{id}/export GET Export PDF Verifies permissions.canCopy. Returns HTTP 403 if forbidden. Calls save_full_for_export(). PdfiumDocument::saveFullForExport
/documents/{id}/export-remote POST Export to remote URL Verifies permissions.canCopy. Streams file to target URL. PdfiumDocument::saveFullForExport
/edits POST Apply PDF edits Maps operation types to permissions (canAnnotate, canModify, canFillForms, canAssemble). Returns HTTP 403 if restricted. Engine edit APIs
/documents/{id}/unlock N/A Dedicated unlock MISSING N/A
/documents/{id}/protect N/A Protect document MISSING N/A

12. End-to-End Export & Reopen Verification Scenarios

SCENARIO A: Unprotected PDF -> Protect with Password -> Export -> Reopen -> Prompted for Password

  • Status: NOT WORKING / IMPOSSIBLE TODAY
  • Reason: "Protect with password" is not implemented anywhere in the backend or engine.

SCENARIO B: Protected PDF -> Enter Password -> Opened -> Export -> Reopen Exported PDF -> No Password Required

  • Status: FULLY WORKING TODAY (Implicitly)
  • Reason: PDFium loads the decrypted PDF structure into memory. Exporting via GET /documents/{id}/export writes the file without encryption. Reopening the exported file requires no password.

SCENARIO C: Protected PDF -> Enter Wrong Password -> Rejected -> Enter Correct Password -> Opened

  • Status: FULLY WORKING TODAY
  • Reason: Invalid password returns HTTP 401 with "Invalid password". The frontend displays "Incorrect password — please try again." and keeps the modal open. Re-submitting with the correct password opens the document cleanly.

13. Security Observations & Risks

  • CONFIRMED FROM CODE — Password Passed in Query String: In gatewayService.ts:L476, the upload URL is constructed as ${this.baseUrl}/documents?password=${encodeURIComponent(password)}. Transmitting passwords in GET/POST URL query parameters poses a security risk because query parameters may be recorded in server access logs or proxy logs.
  • CONFIRMED FROM CODE — Absence of API Rate Limiting: In crud.py:L142-L190, there is no rate-limiting or lock-out mechanism for password validation requests, allowing automated brute-force attempts.
  • CONFIRMED FROM CODE — Implicit Decryption on Export: In export.py:L38, exported PDFs are saved unencrypted. Users who upload a password-protected PDF and subsequently export it will receive an unencrypted file without explicit warning that password protection has been stripped.
  • CONFIRMED FROM CODE — Plaintext Password In-Memory Only: Passwords are passed directly to load_from_memory and are not persisted in document_store or written to disk.

14. Complete Feature Matrix

Feature Status Existing Location Evidence
Detect encrypted PDF IMPLEMENTED pdfium_document.cpp:L142 perms.isEncrypted = (rev != -1)
Password popup IMPLEMENTED PasswordModal.tsx:L15 <PasswordModal state={passwordPrompt} ... />
Validate password IMPLEMENTED pdfium_document.cpp:L42, crud.py:L173 load_from_memory(bytes_data, password)
Wrong password handling IMPLEMENTED crud.py:L183, App.tsx:L650 HTTP 401 "Invalid password" -> UI error
Correct password handling IMPLEMENTED crud.py:L174, App.tsx:L646 Returns DocumentInfoResponse -> Document opens
Render protected PDF IMPLEMENTED pdfium_page.cpp:L50, render.py:L19 Renders tiles/pages post-authentication
OCR protected PDF IMPLEMENTED ocr.py:L63 Executes Tesseract on authenticated doc pages
Edit protected PDF PARTIAL edits.py:L395 Enforces permissions, but doesn't re-encrypt
Export protected PDF PARTIAL export.py:L17 Enforces canCopy, but exports UNENCRYPTED
Remove password PARTIAL export.py:L38 Exporting strips password (implicit, no explicit API)
Export unprotected PDF IMPLEMENTED export.py:L38 save_full_for_export() outputs unencrypted PDF
Reopen unprotected PDF IMPLEMENTED crud.py:L141 Exported file reopens without password
Protect PDF MISSING N/A No code exists to protect/encrypt PDF
Set user password MISSING N/A No functionality to set user password
Set owner password MISSING N/A No functionality to set owner password
AES encryption (detection) IMPLEMENTED pdfium_document.cpp:L155-160 Revision 4/5/6 mapped to "AES-128" / "AES-256"
AES-256 (detection) IMPLEMENTED pdfium_document.cpp:L158 Revision 5/6 mapped to "AES-256"
RC4 (detection) IMPLEMENTED pdfium_document.cpp:L148-153 Revision 2/3 mapped to "RC4-40" / "RC4-128"
Print permission IMPLEMENTED pdfium_document.cpp:L171, store.py:L18 Surfaced in permissions API
Copy permission IMPLEMENTED pdfium_document.cpp:L173, export.py:L30 Enforced on Export (returns HTTP 403)
Modify permission IMPLEMENTED pdfium_document.cpp:L172, edits.py:L375 Enforced on edits (returns HTTP 403)
Annotation permission IMPLEMENTED pdfium_document.cpp:L174, edits.py:L370 Enforced on annotations (returns HTTP 403)
Form permission IMPLEMENTED pdfium_document.cpp:L175, edits.py:L376 Enforced on form fills (allows if permitted)
Extraction permission IMPLEMENTED pdfium_document.cpp:L173, store.py:L21 Surfaced as canCopy
Document assembly IMPLEMENTED pdfium_document.cpp:L177, edits.py:L377 Enforced on page rotate/delete ops
Accessibility permission IMPLEMENTED pdfium_document.cpp:L176, store.py:L24 Surfaced as canExtractForAccessibility
High-quality printing IMPLEMENTED pdfium_document.cpp:L178, store.py:L19 Surfaced as canPrintHighRes
Security UI PARTIAL PasswordModal.tsx, InspectorPanel.tsx Password prompt modal + Inspector security badge exist
Security API PARTIAL crud.py, export.py, edits.py Upload & export handle passwords & perms
C++ security implementation PARTIAL pdfium_document.cpp Document load & permission inspection implemented
Pybind security bindings PARTIAL pdfengine_py.cpp:L252 DocumentPermissions & load_from_memory bound

15. Production Readiness Summary

  • Opening & Viewing Protected PDFs: PRODUCTION READY. Robust, fully tested with unit tests (pdf/engine/tests/document_load_test.cpp) and security integration tests (pdf/tests/security/test_permissions.py).
  • Permission Enforcement: PRODUCTION READY. Gateway correctly returns HTTP 403 Forbidden for restricted edits and exports.
  • Password Removal / Unlocking: NEEDS FEATURIZATION. Works implicitly when exporting, but lacks dedicated API routes and UI buttons for explicit unlock workflows.
  • Protecting / Encrypting PDFs: NOT PRODUCTION READY (0% IMPLEMENTED). Creation of password-protected PDFs or custom permission dictionaries requires adding QPDF or pikepdf encryption writers to the engine/gateway layer.