Importing an archive: leftover keys reach the model constructors, so a crafted file writes into other accounts #354
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#354
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, readingcore/importer.pyand loading hand-made archives into two test accounts.Checked again on 2026-10-01 against
mainat0a5cad587: still present.load()takes out the keys it knows about and hands everything else in an entry straight to a model constructor (Model.objects.create(owner=user, ..., **entry)), and for the profile it callssetattrfor any attribute the profile has. Nothing restricts the keys to the fields the exporter writes. An archive that carriesowner_id,posting_id,application_id,user_id,idor a file field's name therefore sets those columns, and a leftoverowner_idwins over theowner=userpassed beside it, because Django applies the remaining keyword after the related object. That is mass assignment across the ownership boundary.core/importer.py, inload(): the profile loop at:366-371(if hasattr(profile, name) and value: setattr(profile, name, value)); the career sections at:442; skills at:457;restore_contactat:515; companies at:577; listings at:623; applications at:658-663; timeline events at:684(onlyidandcreated_atare removed); interviews at:709-717; offers at:724-731; CVs at:808; letters at:833; uploads at:845; sent documents at:872-877; captures at:902-904. Reached throughcore/management/commands/import_data.py:25-36, which the wiki documents under Backups and your data → Putting an archive back.experienceentry withowner_idset to account B's id creates the row in B's career;posting_idset to one of B's listings attaches A's application to B's listing, whose title, description and notes then appear on A's application page;application_idset to one of B's applications lands in B's timeline;account.profilewithidanduser_idset to B's overwrites B's profile (the headline, and any other field named);account.profile.avatarset to the storage name of B's uploaded CV makesAvatarViewserve B's CV to A; when A's account is later deleted,accounts/deletion.py:files_of()collects that name and deletes B's file from disk.import_datafor them. Also anyone whose file an operator restores without reading it.core/export.pywrites, with every relation resolved only through the importer's own id maps (postings,applications,contacts,resume_mapand so on). Drop anything else before the constructor, and in particular everyid,*_id,owner,userand file field. For the profile, assign only named plain fields; never a relation and never a file field. Restore files only from the archive's own members (avatar_file,logo_file,file), never from a storage name the JSON supplies.Related to #321, whose point 4 covers only the importer skipping the username rules.
Done when
tests/security/test_import_isolation.pyloads an archive whose entries carryowner_id,posting_id,application_id,id,user_idandavatarpointing at a second account, and asserts that the second account's rows, profile and files are unchanged and that every row created belongs to the importing account. It fails today and passes after the fix.tiagoagueda referenced this issue2026-10-03 01:59:01 +00:00