
A PDF that opens without asking for anything can still be encrypted. I ran into this while thinking about upload validation for document tools, so I generated seven two-page test PDFs filled with placeholder contract text and checked them in the browser. Five of them had only a permissions password. Chromium 149's open-source build and Firefox 151 displayed all five straight away, with no prompt and no warning. Yet every one of those files carried a full encryption dictionary. The user password was simply empty, so any reader could decrypt the content on its own. Only one sample, the one with a real open password, stopped at a password box. For a frontend that receives these files, that means "encrypted" is not a single state. It covers files nobody can read without a password and files anyone can read but that ask the viewer to block printing, copying, or editing.
Each viewer enforces permission limits differently
That second group matters because permission flags only work when the viewer chooses to honor them. On the same all-restricted sample, Chromium's built-in viewer blocked printing and copying but still let me draw on the page. Firefox with default settings blocked none of the three. After I turned on its permission enforcement setting, it blocked printing and editing but still did not block copying. Changing the cipher from AES-256 to RC4-128 or RC4-40 changed nothing in those results. Only the permission bits did. So whatever your app shows the user should come from what the file declares, not from what one browser happened to do with it.
Processing tools make their own call on top of that. To see what a real processing tool does, I used ImgIng's PDF compressor (imging.ai is its international site, and my run was on the Chinese deployment) with the permission-only and open-password PDFs I generated. It failed all four encrypted samples I imported with a message that translates to "this PDF is encrypted or permission-protected and needs the protection removed first", and it offered no password field. The page made 46 requests, all of them GET. It did not say why it refuses, and I would only be guessing at the reason.
Three pdf.js calls separate open and permission passwords
I used pdf.js 6.3.289 as a page library. An open password shows up through the onPassword callback. getMetadata() exposes info.EncryptFilterName, which was "Standard" for every permission-only sample and empty for the plain one. getPermissions() returns the granted flags, or null when the file declares none. This is the detection function I ran against all seven files:
async function classifyPdf(bytes) {
const task = pdfjs.getDocument({ data: bytes });
let askedForPassword = false;
task.onPassword = () => { askedForPassword = true; task.destroy(); };
try {
const doc = await task.promise;
const { info: meta } = await doc.getMetadata();
const perms = await doc.getPermissions();
const granted = perms == null ? null : new Set(perms);
return { kind: meta.EncryptFilterName ? 'permission-only' : 'plain', granted };
} catch (reason) {
if (askedForPassword) return { kind: 'open-password' };
throw reason;
}
}
I destroy the task inside onPassword on purpose. The check never asks for a password, and it never holds one. The open-password file then rejects with a PasswordException, and the flag tells the catch block why. Checking PRINT, COPY, and MODIFY_CONTENTS against the granted set gave this, one line per sample:
S0 plain -
S1 permission-only PRINT,COPY,MODIFY_CONTENTS
S3 permission-only COPY
S4 permission-only PRINT
S5 open-password -
S2 and S6 matched S1. The new Set(perms) line is there because of a version difference that bit me. Firefox's built-in pdf.js 5.7.195 returns an array from getPermissions(), while 6.3.289 returns a Set. Code that calls .includes works on the old one and throws on the new one. Normalizing first means the same check runs on both.
This is a check page I built with the same three calls, fed English placeholder copies of three samples. The first card is the all-restricted file, where every row is red except accessibility extraction. The second only blocks copying, so that is the one red row. The third card needs an open password, and the page shows nothing for it except that fact. The page only reads the files and never modifies them.
Report the state and point to the owner
After detection, the frontend should explain what it found and leave the file alone. I have sat through a compliance review, and quietly stripping limits from a file someone else sent is the kind of thing it asks about. I would show "opens normally, but the author restricted printing and copying" for the middle case and "needs the open password" for the last. Then point people to the proper route. They can ask whoever sent the file for an unrestricted copy, or, if the file is theirs and they know the owner password, re-export it from the software that created it. I did not test Acrobat, macOS Preview, or Safari itself, and the WebKit 26.5 run covered only the pdf.js page library. Before adding a PDF upload step, run your own sample files through classifyPdf and write the message for each of the three states first.
Top comments (0)