SEC2-017: JSON from untrusted input is parsed with no nesting bound, so a deeply nested document ends in a RecursionError #959
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#959
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
postuloat56fc9d531, 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 onlyUnicodeDecodeError, ValueError)src/postulo/jobs/views.py:570-574(CompanyPlacesView.postcatches onlyGeoJsonRefused)src/postulo/jobs/logos.py:359-390(_organisation_logoscatches onlyValueError;_walkrecurses over the parsed document)Issue
geojson.readbounds the file (2 MiB), the number of features (2000) and the coordinaterange, but not the nesting depth. CPython's
jsonraisesRecursionErroron a document nesteda few thousand levels deep, and
RecursionErroris not aValueError, so neitherreadnorthe 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) duringFind 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
RecursionErrorbeside
JSONDecodeErrorand caps depth (plugins/europass/reader.py), so the pattern is knownin the codebase.
Impact
A signed-in person uploading such a
.geojsongets 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, catchRecursionErrorwithValueErrorandraise
GeoJsonRefused(_("That file is not GeoJSON.")); inlogos._organisation_logos, catch(ValueError, RecursionError)and skip the block, then apply a depth cap to the parseddocument before
_walk(or make_walkiterative). Done whentests/test_geojson.pyuploadsa deeply nested array and gets the refusal message, and
tests/test_logos.pyfeeds a page withsuch 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.