July 11, 2026

PDFs uploaded by users should be treated as untrusted files. A document may have come from a scanner or a word processor, but a PDF is capable of containing much more than the text and images visible on its pages.

A valid PDF can include JavaScript, actions that run when the document is opened, embedded files, annotations, rich media, and references to external resources. Some of these features can cause a PDF reader to perform actions the user did not expect. A maliciously constructed file can also be unusually expensive to parse or exploit weaknesses in the software processing it.

The goal is to remove everything we do not need and leave only the document itself. We sanitize the PDF on the uploader’s device, before sending it anywhere else, so that downstream systems receive a newly created document rather than the original file. The original PDF is treated as potentially hostile input and is never passed through unchanged.

Basic filtering

If the bytes cannot be loaded as a PDF, reject the file.

Swift (iOS):

guard let inputDoc = PDFDocument(data: inputData) else { return nil }

Kotlin (Android):

try {
    val document = PDDocument.load(java.io.ByteArrayInputStream(bytes))
    if (document.isEncrypted) {
        document.setAllSecurityToBeRemoved(true)
    }
} catch (e: Exception) {
    // reject
}

PDDocument.load throws if the bytes are not a PDF. If the file loads and is encrypted, security restrictions are removed so the rest of the sanitizer can read it. That encryption step exists only on Android.

Remove exploits on iOS

On iOS we can take what should be visible, draw it into a new document, and throw the original container away. Each page is drawn into a new PDF context. Vector content stays vector. JavaScript, actions, and the rest of the original object tree are not copied.

func flattenPDFKeepingVectors(inputData: Data) -> Data? {
    guard let inputDoc = PDFDocument(data: inputData) else { return nil }

    let pageCount = inputDoc.pageCount
    guard let firstPage = inputDoc.page(at: 0) else { return nil }
    var pageRect = firstPage.bounds(for: .mediaBox)

    let outputData = NSMutableData()
    guard let consumer = CGDataConsumer(data: outputData as CFMutableData),
          let context = CGContext(consumer: consumer, mediaBox: &pageRect, nil) else {
        return nil
    }

    for i in 0..<pageCount {
        guard let page = inputDoc.page(at: i) else { continue }

        context.beginPage(mediaBox: &pageRect)
        context.saveGState()
        page.draw(with: .mediaBox, to: context)
        context.restoreGState()
        context.endPage()
    }

    context.closePDF()
    return outputData as Data
}

Remove exploits on Android

Android does not have an API that makes this as easy. We have no choice but to parse the loaded document with PDFBox: delete JavaScript names, strip /OpenAction, drop launch annotations, remove scripts from form fields while keeping the fields themselves, and delete page-level /AA, /RichMedia, and /Launch keys. Then we save a new byte array.

val document = PDDocument.load(ByteArrayInputStream(inputBytes))
try {
    document.documentCatalog.setOpenAction(null)
    document.documentCatalog.setActions(null)

    for (page in document.pages) {
        val cos = page.cosObject
        cos.removeItem(COSName.getPDFName("AA"))
        cos.removeItem(COSName.getPDFName("RichMedia"))
        cos.removeItem(COSName.getPDFName("Launch"))
    }

    val output = ByteArrayOutputStream()
    document.save(output)
    output.toByteArray()
} finally {
    document.close()
}

Keep reading

  1. Shoulder Surfing QR Codes: A Defense

    August 19, 2026

    QR codes are a convenient way to pair a mobile device with a desktop application or a web application running on the desktop: the desktop displays a code, and the phone scans it. …

    Continue reading

  2. Sanitizing Images Uploaded by Users

    June 27, 2026

    Images uploaded by users should be treated as untrusted files. We focus on JPEG because, in most cases, an image uploaded by a user can simply be converted to JPEG first (if …

    Continue reading

Entelechy, (from Greek entelecheia), in philosophy, that which realizes or makes actual what is otherwise merely potential. — Encyclopedia Britannica