SEC2-058: DAV responses are read whole with no size limit and no total deadline #33
Labels
No labels
bug
documentation
enhancement
performance
security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-dav#33
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-10-16 in a read-only security review of
postulo-davat1d49d6e, checked by a second, independent pass.Severity: Low · Category: Input Validation · Reference: CWE-400 (Uncontrolled Resource Consumption); CWE-770 (Allocation of Resources Without Limits or Throttling); OWASP A04:2021 (Insecure Design)
Location:
src/postulo_dav/dav.py:107-108(Server.request:self.client.request(...), which has read the whole body before it returns)src/postulo_dav/dav.py:110-122(Server.propfind:ElementTree.fromstring(response.content))src/postulo_dav/dav.py:223-227(Server.get:Resource(response.text, ...))src/postulo_dav/dav.py:284-293(connect:postulo_client()with nototal)src/postulo/plugins/http.py:324-337(client(timeout, total)),:479-527(get_bytes,read_body)Issue
Every request goes through the guarded client, so the destination policy holds, and
defusedxmlrefuses entity expansion and external entities. What is missing is the bound the core added for
every other body it reads from another server (postulo/postulo#321, #781, #782):
client.request()buffers the whole response beforeresponse.contentorresponse.textistouched, so a PROPFIND answer or a card or event of any size is held in memory in full, then
parsed in full (
tree.iter(...),text.properties(...)); andconnect()opens the clientwithout
total, so each wait is held to 10 s but their sum is not. A server answering a byteevery few seconds holds the scheduler's pass for as long as it likes. The core's client states
the rule in its docstring ("A body is read with
read_body, or not at all");defusedxmlboundsentities, not document size.
Impact
The person's own DAV server, or whoever can write to the collection the connection reads, can
make a scheduled pass allocate without bound or run without end; syncs run inline in the
scheduler pass and under Sync now in a web worker, so every later connection and reminder
waits. Denial of service against a personal instance is out of scope in the threat model, hence
Low; but the core holds plugins to a bounded read as a rule, and this official plugin does not
follow it.
Fix
Prerequisite in the core:
read_body,deadline_in,untilandBodyRefusedlive inpostulo.plugins.httpand are not published onpostulo.plugins.api, so this plugin cannotimport them without failing
tests/test_surface.py. The core must first put them on the pluginsurface (a small core issue; the paperless and France Travail plugins want the same). Then:
connect()openspostulo_client(total=60.0)(this part needs nothing new and can land now);propfindandgetstream and stop at a limit:with 2 MB for a multistatus and 256 KB for one card or event, both generous;
BodyRefusedbecomes aDavErrornaming the resource, so the run reports it and moves on.Done when a test serves a card over the limit and a multistatus over the limit and asserts that
the run reports each and continues with the next resource, and the fake server's
MockTransportin
tests/test_dav.pystill works throughstream().Related
#5 and #6 (open) mention streaming for the future file store and backup destination, not the
sync's PROPFIND and GET bodies; #13 (open) confines origins and methods, not sizes.
Fixed by pull request #35, merged as
fb8bbd7ae.