SEC2-011: Free text written through the API has no length bound where the listing history has one #953
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#953
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: Informational · Category: Input Validation · Reference: CWE-770 (Allocation of Resources Without Limits or Throttling); OWASP A04:2021 (Insecure Design)
Location:
src/postulo/api/schemas.py:849(EventIn.body),:835(StatusIn.note),:760(InterviewOutcomeIn.note),:743and:755(InterviewIn.notes,InterviewPatch.notes),:1072(LetterIn.body),:362,:386,:462(company and contactnotes),:619(ListingIn.description)src/postulo/api/schemas.py:564-568(ListingEventIn.body, bounded toBODY_MAX_CHARS)src/postulo/applications/services.py:61-68(record_eventclipssummaryandactor, notbody)src/postulo/jobs/history.py:93-98, 155-156(bounded, and the rule the listing history follows)Issue
#218 and #270 settled that text arriving through the API is held to a length: a listing's
history entry refuses a
bodyover 40,000 characters rather than cutting it. The otherfree-text fields are bare
str:and the service behind them clips only the columns PostgreSQL would refuse:
The only ceiling is Django's
DATA_UPLOAD_MAX_MEMORY_SIZE(2.5 MB by default; the APIdocuments it as
too-large), so each of these calls stores up to that per request, 600 times anhour per token. The same fields on the pages are
TextFieldtextareas, so the page is nostricter; but a mail client or an agent is the client that pastes whole messages, and the
timeline body is then drawn on every page, export and theme that shows the application.
Impact
A token with
writefills the database and backups at megabytes per call with no refusal; afiled message of arbitrary size lands on pages and in exports that were written for a
paragraph. Denial of service against a personal instance is out of the threat model's
scope, which is what keeps this Informational; the inconsistency with the rule the project set
for the listing history is the finding.
Fix
Mirror what the listing history does today (
history.py:93-98and:155-156): refuse at theAPI and cut-with-marker on the page path.
Field(max_length=BODY_MAX_CHARS)on eachbody/notes/note/descriptionabove (reusingpostulo.jobs.history.BODY_MAX_CHARS,already imported in
schemas.py) makes the API refuse with a 422;bounded(body)inrecord_event, asrecord_listing_eventdoes, cuts the page path at the same number with themarker. Done when
tests/test_api.pyposts a 40,001-character timeline body and gets a 422naming
body, the OpenAPI description shows themaxLength, and a page test sees a longerbody stored cut with the marker.
Related
#218 and #463 (closed) - set the two rules quoted here. #766 (closed) - numeric ranges, the same idea for numbers.