Security review fixes: bounded answers, dead token, declined files, duplicate id, repinned lock #18

Merged
tiagoagueda merged 2 commits from sec2-fixes into main 2026-10-19 09:52:02 +00:00
Owner

What and why

  • #16 (SEC2-061) every request is streamed through one helper: read_body (1 MiB, 30 s; upload 60 s) inside until(deadline_in(...)), the client opened with total=, follow_redirects=False (the plugin never follows a next link or a redirect off the configured host; it only ever asks for page_size=1). Over-cap, too-slow and redirected answers are a PaperlessError.
  • #13 a 401 raises ConnectionUnusable (secrets not kept), so the connection is switched off once with a readable reason; test() still returns a TestResult; 403 stays an ordinary failure.
  • #14 _explain flattens DRF field errors (document: ...), takes list bodies, and folds every branch to one line of at most 120 characters (the 2026-10-16 comment); a 400 on the document field makes put return None, so the copy ends DECLINED after one upload.
  • #11 the duplicate id is the last (#id) of the message, not the first #digits (a title may hold one). The fake now answers in Paperless's full wording.
  • #12 the custom field is written only for an absolute http(s) address; a failure to create or write it is logged and the reference is still returned. README says the link needs POSTULO_PUBLIC_URL.
  • #10 ngettext for the Test result; catalogues re-extracted, nothing translated.
  • #17 (SEC2-063) dev group repinned to core 42be52048, uv lock --upgrade (pyjwt 2.15.1, urllib3 2.8.0, oauthlib 4.0.0); pip-audit --strict over the export (without the project and without postulo, a git dependency) is clean. CI installs uv with the checksum-pinned installer (both workflows) and has a new audit job. The repin makes the extraction add seven scaffold catalogues.

Tests

65 pass (was 37); ruff check/format, extract --check, check, guard clean. 22 of the new or changed tests fail with the old store.py (stashed).

Left open / notes

Nothing left open. Observed in the core, not fixed here: inside one send_pending() pass, the second copy of a connection that has just been retired is still tried once, because the pass holds the connection object it read earlier; the next pass does not dial it. The test asserts that.

Closes #10, #11, #12, #13, #14, #16, #17

🤖 Generated with Claude Code

## What and why - **#16 (SEC2-061)** every request is streamed through one helper: `read_body` (1 MiB, 30 s; upload 60 s) inside `until(deadline_in(...))`, the client opened with `total=`, `follow_redirects=False` (the plugin never follows a `next` link or a redirect off the configured host; it only ever asks for `page_size=1`). Over-cap, too-slow and redirected answers are a `PaperlessError`. - **#13** a 401 raises `ConnectionUnusable` (secrets not kept), so the connection is switched off once with a readable reason; `test()` still returns a `TestResult`; 403 stays an ordinary failure. - **#14** `_explain` flattens DRF field errors (`document: ...`), takes list bodies, and folds every branch to one line of at most 120 characters (the 2026-10-16 comment); a 400 on the `document` field makes `put` return `None`, so the copy ends DECLINED after one upload. - **#11** the duplicate id is the last `(#id)` of the message, not the first `#digits` (a title may hold one). The fake now answers in Paperless's full wording. - **#12** the custom field is written only for an absolute http(s) address; a failure to create or write it is logged and the reference is still returned. README says the link needs `POSTULO_PUBLIC_URL`. - **#10** `ngettext` for the Test result; catalogues re-extracted, nothing translated. - **#17 (SEC2-063)** dev group repinned to core `42be52048`, `uv lock --upgrade` (pyjwt 2.15.1, urllib3 2.8.0, oauthlib 4.0.0); `pip-audit --strict` over the export (without the project and without `postulo`, a git dependency) is clean. CI installs uv with the checksum-pinned installer (both workflows) and has a new `audit` job. The repin makes the extraction add seven scaffold catalogues. ## Tests 65 pass (was 37); ruff check/format, `extract --check`, `check`, `guard` clean. 22 of the new or changed tests fail with the old `store.py` (stashed). ## Left open / notes Nothing left open. Observed in the core, not fixed here: inside one `send_pending()` pass, the second copy of a connection that has just been retired is still tried once, because the pass holds the connection object it read earlier; the next pass does not dial it. The test asserts that. Closes #10, #11, #12, #13, #14, #16, #17 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The dev group pinned the core 210 commits back, and its lock carried pyjwt 2.13.0, urllib3 2.7.0
and oauthlib 3.3.1 with nineteen published advisories between them. The pin is now the core's
main at 42be52048 and `uv lock --upgrade` brought pyjwt 2.15.1, urllib3 2.8.0 and oauthlib 4.0.0;
pip-audit --strict over the export of the lock (without the project and without Postulo, a git
dependency that is not on PyPI) finds nothing. The comment that promised `@v0.3.0` now says
what the pin is.

CI installs uv the way the core does: a pinned version whose install.sh is checked against its
sha256 before it runs, in both workflows, instead of `curl | sh` of whatever the address
answers. A new `audit` job runs pip-audit over the lock on every push so the next drift is
reported.

The new core offers seven more languages, so the catalogues are extracted again: seven new
scaffold catalogues, no translation written.

Closes #17

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Read Paperless's answers within a deadline and a cap, retire a dead token, decline unconsumable files, and stop mis-reading the duplicate's id
All checks were successful
CI / Audit the locked dependencies (pull_request) Successful in 1m33s
CI / test (pull_request) Successful in 2m2s
3a60e1f0a8
Every request now goes through one streamed helper: the answer is read with the core's
`read_body` (1 MiB, 30 s; the upload 60 s), the client is opened with `total=`, and a
redirect is never followed, so a server the account holder named can neither hold the
scheduler nor be sent the token a second time elsewhere (#16).

A 401 raises `ConnectionUnusable`, so a revoked token switches the connection off once with a
sentence the person can read instead of failing every document six times; the Test button
still answers with a result. A 403 stays an ordinary failure (#13).

`_explain` flattens Django REST Framework's field errors, accepts a list body, and folds every
branch onto one line of at most 120 characters, so a server cannot write lines of its own into
the operator's log. A 400 on the `document` field is a refusal of the file itself: `put`
returns None and the copy ends as not accepted after one upload, not six (#14).

The duplicate's id is read from the end of Paperless's message, `(#id)`, and no longer from the
first `#digits` in a title that may be the person's own text, which attached the copy to an
unrelated document and overwrote its link (#11).

The link back to the application is written only when it is a whole address (without
POSTULO_PUBLIC_URL it is a bare path Paperless refuses), and a failure to write it is logged,
not raised, so an archived document is not shown as failed and uploaded again (#12).

The Test result uses ngettext, so one document is "1 document" and languages with several
plural forms can translate it (#10). The catalogues are extracted; nothing is translated here.

Closes #10, #11, #12, #13, #14, #16

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-paperless!18
No description provided.