You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Post-processing tasks (the small size filter, and the class masking coming in #999) accept a scope identifier — occurrence_id or source_image_collection_id — in their config and operate on the matching rows. The task confirms the scope object exists, but not that it belongs to the project the job runs under. Today the only trigger is the Django admin, restricted to superusers, so this is a latent gap rather than an exploitable bug. It is worth closing before a less-privileged trigger surface (the deferred REST API) lands.
Where
ami/ml/post_processing/small_size_filter.py — _scoped_detections() does Occurrence.objects.filter(pk=...).exists() with no project check.
The task's run() has no project context wired in today; the job's project would need to be threaded in to validate against it.
Why it matters
The admin action derives the job's project from the selected row, so in the admin flow the scope object and the project are consistent by construction. The task itself, though, trusts whatever occurrence_id / source_image_collection_id sits in Job.params. A job created by another path — a future REST endpoint, a management command, or hand-edited params — could point the scope at an object in a different project, and the task would process it without complaint. That is a cross-project write gap once a non-superuser trigger exists.
Proposed direction (to discuss)
Thread the job's project into the task (it already holds self.job) and assert the scope object's project matches, raising a clear error otherwise; or
Resolve the scope queryset through the project (e.g. project.occurrences.filter(pk=...)) so a mismatch yields a not-found result and the existing error path fires.
Add a test: an occurrence/collection from project B with a job on project A should make the task raise.
Deferred from #1289 (scaffolding) and #999 (class masking) — both ship the .exists()-only check. Surfaced during the #1289 pre-merge review.
Summary
Post-processing tasks (the small size filter, and the class masking coming in #999) accept a scope identifier —
occurrence_idorsource_image_collection_id— in their config and operate on the matching rows. The task confirms the scope object exists, but not that it belongs to the project the job runs under. Today the only trigger is the Django admin, restricted to superusers, so this is a latent gap rather than an exploitable bug. It is worth closing before a less-privileged trigger surface (the deferred REST API) lands.Where
ami/ml/post_processing/small_size_filter.py—_scoped_detections()doesOccurrence.objects.filter(pk=...).exists()with no project check.ami/ml/post_processing/class_masking.py(lands with Let admins mask classifier predictions to a species list #999) — the occurrence scope uses the same.exists()-only pattern.run()has no project context wired in today; the job's project would need to be threaded in to validate against it.Why it matters
The admin action derives the job's
projectfrom the selected row, so in the admin flow the scope object and the project are consistent by construction. The task itself, though, trusts whateveroccurrence_id/source_image_collection_idsits inJob.params. A job created by another path — a future REST endpoint, a management command, or hand-edited params — could point the scope at an object in a different project, and the task would process it without complaint. That is a cross-project write gap once a non-superuser trigger exists.Proposed direction (to discuss)
self.job) and assert the scope object's project matches, raising a clear error otherwise; orproject.occurrences.filter(pk=...)) so a mismatch yields a not-found result and the existing error path fires.Deferred from #1289 (scaffolding) and #999 (class masking) — both ship the
.exists()-only check. Surfaced during the #1289 pre-merge review.