必须校验文件头和MIME类型,仅靠扩展名或Content-Type不可信;真实PDF需前4字节为%PDF且magic识别为application/pdf,同时限制上传大小、使用tempfile安全落盘、解密前检测加密状态,并优先用fitz处理以保障鲁棒性与安全性。

上传前必须校验文件头和MIME类型
仅靠文件扩展名或Content-Type header 判断PDF不可信,攻击者可伪造application/pdf并上传恶意二进制(如带JS的PDF或嵌套EXE)。真实校验需读取文件头4字节是否为%PDF(ASCII值25 50 44 46),再用python-magic确认实际类型。
示例做法:
import magic from pathlib import Path <p>def is_valid_pdf(file_bytes: bytes) -> bool: if len(file_bytes) < 4: return False if file_bytes[:4] != b"%PDF": return False try: mime = magic.from_buffer(file_bytes, mime=True) return mime == "application/pdf" except Exception: return False
- 不要用
file.content_type做唯一判断,它由前端或HTTP库生成,完全可控 - 限制上传大小(如
MAX_UPLOAD_SIZE = 10 * 1024 * 1024),防止内存耗尽或DoS - 若用Flask/FastAPI,务必在
request.files读取后立即校验,避免先保存再检查
解密前必须检测是否加密且密码正确
用户上传的PDF可能被加密(尤其是AES-256),直接传给pdfplumber.open()或fitz.open()会抛出fitz.FileDataError或静默失败——前者崩溃,后者返回空文本。必须显式调用is_encrypted并尝试decrypt(),且要区分“密码错误”和“权限不足”。
PyPDF2的典型处理链:
立即学习“Python免费学习笔记(深入)”;
from PyPDF2 import PdfReader
<p>reader = PdfReader(uploaded_file_path)
if reader.is_encrypted:
try:</p><h1>尝试空密码(常见于只设权限密码的PDF)</h1><pre class="brush:php;toolbar:false;"> if reader.decrypt("") == 0:
# 再试业务预设的统一解密密钥(如从配置读取)
if reader.decrypt(settings.PDF_DECRYPT_KEY) == 0:
raise ValueError("PDF decryption failed")
except Exception as e:
raise ValueError(f"Invalid encrypted PDF: {e}")-
decrypt()返回0表示失败,1或2才成功(2表示权限密码,1是用户密码) - 绝不能把用户输入的密码原样传给
decrypt()——这等于开放暴力破解接口;应只接受内部密钥或跳过解密(转用支持加密PDF的fitz) -
pdfplumber不自动解密,必须先用PyPDF2或fitz解密后再传入
提取文本时优先用fitz而非pdfplumber处理加密/扫描PDF
当PDF含扫描图像、加密流或损坏交叉引用表时,pdfplumber常卡死或返回空字符串,而fitz(PyMuPDF)底层用C实现,自带解密和OCR fallback能力,更鲁棒。
关键差异:
-
fitz.open()能直接打开加密PDF(只要提供密码),无需前置解密步骤 -
page.get_text("text")比pdfplumber.Page.extract_text()更快,且对中文排版支持更稳 - 若PDF是扫描件,
fitz可配合pytesseract调用OCR:page.get_text("ocr")(需提前安装tesseract)
安全提示:启用OCR前必须确认图像区域不含敏感水印或页眉——OCR会一并提取,建议先用page.get_image_info()过滤掉含公司logo的图片。
临时文件必须隔离存储且自动清理
Web应用中,用户上传的PDF若直接写入/tmp或项目目录,易引发路径遍历(如文件名../../etc/passwd)或磁盘占满。必须用tempfile.NamedTemporaryFile(delete=False)生成随机路径,并限定父目录为独立挂载卷(如/var/tmp/pdf_uploads)。
清理策略:
- 使用
atexit.register()或异步任务(Celery/asyncio)确保进程退出前删除 - 对每个临时文件加
os.chmod(path, 0o600),禁止其他用户读取 - 绝不把临时路径回传给前端(如
src="/tmp/xxx.pdf"),防止越权访问
最危险的疏漏是:在try/except中打开PDF后忘记doc.close()——fitz会锁住文件句柄,导致后续os.remove()失败,最终填满磁盘。



















