SEC2-017: JSON from untrusted input is parsed with no nesting bound, so a deeply nested document ends in a RecursionError #959

Closed
opened 2026-10-17 10:38:20 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo at 56fc9d531, checked by a second, independent pass.

Severity: Low · Category: Input Validation · Reference: CWE-674 (Uncontrolled Recursion); OWASP A05:2021 (Security Misconfiguration)

Location:

  • src/postulo/jobs/geojson.py:97-100 (read: json.loads(...) catches only UnicodeDecodeError, ValueError)
  • src/postulo/jobs/views.py:570-574 (CompanyPlacesView.post catches only GeoJsonRefused)
  • src/postulo/jobs/logos.py:359-390 (_organisation_logos catches only ValueError; _walk recurses over the parsed document)

Issue

geojson.read bounds the file (2 MiB), the number of features (2000) and the coordinate
range, but not the nesting depth. CPython's json raises RecursionError on a document nested
a few thousand levels deep, and RecursionError is not a ValueError, so neither read nor
the view catches it and the upload answers 500. The same shape in a JSON-LD block of a
company's website reaches logos._organisation_logos (and _walk, itself recursive) during
Find logo; the errand runner logs it as a failed errand rather than answering with the
"could not be used as a logo" sentence. The Europass reader already catches RecursionError
beside JSONDecodeError and caps depth (plugins/europass/reader.py), so the pattern is known
in the codebase.

Impact

A signed-in person uploading such a .geojson gets a server error instead of a refusal
(nothing is written; only their own request fails). A company website can make Find logo
fail with a logged traceback instead of a sentence. Robustness, not confidentiality.

Fix

Follow the Europass pattern: in geojson.read, catch RecursionError with ValueError and
raise GeoJsonRefused(_("That file is not GeoJSON.")); in logos._organisation_logos, catch
(ValueError, RecursionError) and skip the block, then apply a depth cap to the parsed
document before _walk (or make _walk iterative). Done when tests/test_geojson.py uploads
a deeply nested array and gets the refusal message, and tests/test_logos.py feeds a page with
such a JSON-LD block and gets the "nothing could be used" sentence.

#634 (closed) - fixed the Europass reader's RecursionError; the precedent. #769 (closed) - other crafted inputs that crashed with an unhandled exception; it lists other parsers, not these two. #880 (closed) - the GeoJSON import; did not bound depth.

Found on 2026-10-16 in a read-only security review of `postulo` at `56fc9d531`, checked by a second, independent pass. **Severity:** Low · **Category:** Input Validation · **Reference:** CWE-674 (Uncontrolled Recursion); OWASP A05:2021 (Security Misconfiguration) **Location:** - `src/postulo/jobs/geojson.py:97-100` (`read`: `json.loads(...)` catches only `UnicodeDecodeError, ValueError`) - `src/postulo/jobs/views.py:570-574` (`CompanyPlacesView.post` catches only `GeoJsonRefused`) - `src/postulo/jobs/logos.py:359-390` (`_organisation_logos` catches only `ValueError`; `_walk` recurses over the parsed document) ## Issue `geojson.read` bounds the file (2 MiB), the number of features (2000) and the coordinate range, but not the nesting depth. CPython's `json` raises `RecursionError` on a document nested a few thousand levels deep, and `RecursionError` is not a `ValueError`, so neither `read` nor the view catches it and the upload answers 500. The same shape in a JSON-LD block of a company's website reaches `logos._organisation_logos` (and `_walk`, itself recursive) during *Find logo*; the errand runner logs it as a failed errand rather than answering with the "could not be used as a logo" sentence. The Europass reader already catches `RecursionError` beside `JSONDecodeError` and caps depth (`plugins/europass/reader.py`), so the pattern is known in the codebase. ## Impact A signed-in person uploading such a `.geojson` gets a server error instead of a refusal (nothing is written; only their own request fails). A company website can make *Find logo* fail with a logged traceback instead of a sentence. Robustness, not confidentiality. ## Fix Follow the Europass pattern: in `geojson.read`, catch `RecursionError` with `ValueError` and raise `GeoJsonRefused(_("That file is not GeoJSON."))`; in `logos._organisation_logos`, catch `(ValueError, RecursionError)` and skip the block, then apply a depth cap to the parsed document before `_walk` (or make `_walk` iterative). Done when `tests/test_geojson.py` uploads a deeply nested array and gets the refusal message, and `tests/test_logos.py` feeds a page with such a JSON-LD block and gets the "nothing could be used" sentence. ## Related #634 (closed) - fixed the Europass reader's `RecursionError`; the precedent. #769 (closed) - other crafted inputs that crashed with an unhandled exception; it lists other parsers, not these two. #880 (closed) - the GeoJSON import; did not bound depth.
tiagoagueda added this to the 0.6.0 milestone 2026-10-17 10:38:21 +00:00
Sign in to join this conversation.
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#959
No description provided.