Harden PDF uploads: size cap, magic-byte check, non-blocking parse

read_pdf_upload() bounds reads at 10 MB and requires the %PDF magic
before anything reaches pdftotext (SEC-06); rejections land in the
per-file errors list with the filename. Parsing now runs via
asyncio.to_thread so a slow PDF no longer blocks the event loop for up
to 30s (BE-14). Review notes: temp files were already context-managed
(BE-09 overstated) and the pension batch already commits atomically
(BE-10 overstated). 69 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-10 15:00:49 -06:00
parent 3cdd7d9eab
commit 82a7bfc4c5
4 changed files with 77 additions and 4 deletions

View File

@@ -1,10 +1,13 @@
from datetime import date
import asyncio
from fastapi import APIRouter, Depends, UploadFile
from pydantic import BaseModel
from sqlmodel import Session, select
from app.auth import get_current_user
from app.uploads import read_pdf_upload
from app.db import get_session
from app.models.models import Bank, PensionSnapshot, PensionSnapshotRead
from app.services.pension_pdf import parse_pension_pdf
@@ -115,8 +118,11 @@ async def upload_pension_pdfs(
for file in files:
filename = file.filename or "unknown.pdf"
try:
pdf_bytes = await file.read()
fund_snapshots = parse_pension_pdf(pdf_bytes, filename)
pdf_bytes = await read_pdf_upload(file)
# pdftotext runs as a subprocess (up to 30s); keep the event loop free
fund_snapshots = await asyncio.to_thread(
parse_pension_pdf, pdf_bytes, filename
)
except ValueError as e:
errors.append(str(e))
continue