iCalendar: the interview UID is written unescaped and an unknown outcome raises, and an imported archive sets both #450
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#450
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-09-29 in a read-only review of postulo at
95348908d, reading the iCalendar writer and the archive importer and rendering the feeds in a test database.Checked again on 2026-10-01 against
mainat0a5cad587: still present.Since #218 every typed value on an iCalendar line goes through
escape()orwithout_controls(), except the interview'sUID, which is written as stored.calendar_status()looks the outcome up in a four-entry table with no fallback. Neither value can be set through a form or the API, but the archive importer restores both from the file without checking them:uidwhenever it does not collide, andoutcomeandkindthrough**interview_entry, where companies, CVs and listing events have their kinds checked.applications/ical.py:172,event_lines:f"UID:{interview.uid}", with nowithout_controls.applications/ical.py:106-113,calendar_status:return STATUS_OF[interview.outcome].core/importer.py:706-717:uid = interview_entry.pop("uid", None)andInterview.objects.create(..., **({"uid": uid} if uid else {}), **interview_entry). Kinds are checked only at:564(CompanyKind),:806(CVKind) and:965(ListingEventKind).plugins/api.py:186-191) and used bypostulo-dav(src/postulo_dav/sync.py:179,:189), which also builds the event's address from the uid (:576)./applications/interviews/<pk>/calendar.icsanswer 500 withKeyError. The diary feed is not affected, because it carries only interviews still scheduled; the CalDAV sync, which walks every recent interview whatever its outcome, raises on it./applications/interviews/calendar.icsand/api/v1/interviews/calendar.icswhile the interview is upcoming: the class of injection #218 closed for every other field.manage.py import_datainto somebody's account; the effect stays within that account's own feeds and calendar.applications/endings.py:71-79,_label), and this one raises instead.ical.py, writeUID:{without_controls(interview.uid)}and useSTATUS_OF.get(interview.outcome, "CONFIRMED"). In the importer, replace a uid that does not match a safe pattern (for example^[A-Za-z0-9@._-]{1,64}$) with a fresh one, and map an unknownoutcomeorkindtoSCHEDULEDandOTHER, as listing events already are.Related to #218 (closed), which escaped every other value on these lines.
Done when
tests/security/test_feeds_and_pictures.pyhas a test with an interview whose uid contains\r\n, expecting no extra line in the feed, and one with an unknown outcome, expecting the single-interview file to answer 200.tests/test_backup.pythat imports an archive with an unsafe uid and an unknown outcome and expects a fresh uid and a known outcome.