## Findings ### High — Upload limits apply only after the complete JSON body is buffered - **Refs:** `src/jarvis_finance/api/schemas/postfinance.py:8-21` (`PostFinanceImportRequest`); `src/jarvis_finance/api/main.py:93-100` (`block_untrusted_writes`) - The Pydantic string limits are evaluated only after Starlette/FastAPI has read and decoded the entire request body. There is no route-level `Content-Length` or streaming-body cap. Pydantic also ignores unknown fields by default, so an arbitrarily large extra JSON property bypasses all declared field limits. - Even a valid maximum upload temporarily holds roughly 82 MB of Base64 text plus 62 MB of decoded bytes before ZIP/PDF parsing begins. Concurrent requests can therefore exhaust process memory. - **Recommendation:** enforce a hard request-body limit before JSON parsing, reject missing/oversized `Content-Length`, set `extra="forbid"`, and add concurrency/rate limits for these expensive endpoints. ### High — PDF limits do not bound page count or decompressed/extracted content - **Refs:** `src/jarvis_finance/imports/postfinance_documents.py:214-236` (`_pdf_text`), especially the new extraction fallback at `224-229`; ZIP processing at `713-737` - The 12 MB raw-file limit does not cap PDF page count, decompressed stream size, extraction output, or extraction time. A small malicious PDF can contain many pages or highly compressed content and consume excessive CPU/RAM. The new fallback may run extraction twice on a problematic page. - Across a ZIP, up to 100 PDFs and 100 MB of declared uncompressed input are accepted, multiplying this exposure. - **Recommendation:** cap pages per PDF and per bundle, cap accumulated extracted characters/bytes, reject excessive compression ratios/object counts where feasible, and process extraction under a timeout/resource budget. ### Medium — Unclassified parser errors are returned verbatim and now displayed - **Refs:** `src/jarvis_finance/api/routers/postfinance.py:21-48` (`_safe_error_message`, `_run`); `src/jarvis_finance/imports/postfinance_documents.py:230-233` (`_pdf_text`); `frontend/src/api/client.ts:42-50` - `_safe_error_message()` returns the original message for every unmatched `ValueError`. `_pdf_text()` explicitly rethrows all `ValueError`s, including ones originating inside `pypdf`. The frontend change now renders backend `detail` directly. - This defeats the intended sanitization boundary and risks exposing parser internals or source-derived details. - **Recommendation:** make the sanitizer default to a generic bounded message, map only explicitly recognized application error codes, and log detailed exceptions only to a protected sink without source bytes or filenames. ### Medium — Preview reference contract is inconsistent - **Refs:** `src/jarvis_finance/services/postfinance_service.py:503-508` (`preview_postfinance_import`); `src/jarvis_finance/api/schemas/postfinance.py:70`; `frontend/src/api/postfinance.ts:54`; `frontend/src/pages/PostFinancePage.vue` preview reference rendering - Backend and schema return `source_references: {"upload": ...}`, while the frontend expects a scalar `display_reference`. - The preview UI will render an undefined reference despite a successful preview. - **Recommendation:** standardize one contract and add an API/component assertion for the exact response field. ## Reviewed boundaries - **Preview read-only:** No direct persistence or filesystem write was found in the preview path; the newly added checks perform parsing and database reads only. - **Confirm fail-closed:** Confirm reparses the source, verifies preview IDs/database revision, and checks `confirm_allowed` before new persistence. No new write-gate bypass was found. The duplicate path returns idempotently without new writes. ## Verification / scope - Reviewed the current tracked `git diff` against base `8c4b9e8`; no files were created or modified. - `git diff --check` passed. - Targeted tests could not run: the worktree has no `.venv/bin/python`, and the available Python environment does not have `pytest`. - The untracked test file was not reviewed because the assignment specified **git diff only**.