Skip to content

Commit c378718

Browse files
MartinCastroAlvarezmartin-castro-laminr-aiclaude
authored
docs(security): file/image field storage-backend sharp edge — Closes #145 (#150)
Architect post-hoc audit follow-up from #119: PR #110 (FileField / ImageField read-half) emits ``value.url`` in the detail envelope. The URL is the consumer's storage-backend choice: signed-URL backends ship time-bound URLs (safe by construction); local-storage backends with publicly-readable ``MEDIA_URL`` ship a path that any staff user can share with anyone, including unauthenticated parties. This is not a regression (Django's HTML admin behaves the same way with ``<a href="{{ field.url }}">``), but the SPA makes the URLs trivially scriptable, which raises the operational stakes of ``MEDIA_URL`` configuration. ``SECURITY.md`` didn't surface it. Adds a "File / image field storage" subsection to §9 "Recommended consumer settings" enumerating the two storage classes and the concrete consumer actions for the local-storage case (django-private-storage, nginx auth_request, or a custom staff-gated MEDIA_ROOT view). Tier 5 — touches ``SECURITY.md``. Authored by the Security & Compliance Lead session; merger must be the repo owner. P1 — should land before the next release cycle so the disclosure is honest with consumers. Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b8be316 commit c378718

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

SECURITY.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,39 @@ equivalent) is recommended once the SPA is live; sample snippet will
212212
ship in `docs/installation.md` alongside PR #6
213213
(see `docs/agents/open-questions.md` QSEC-2026-05-25-03).
214214

215+
### File / image field storage
216+
217+
The package emits `value.url` for `FileField` and `ImageField` values
218+
in the detail response. Whether that URL is appropriately gated
219+
depends on the consumer's storage backend — the package never wraps
220+
the URL with its own access check:
221+
222+
- **Signed-URL backends** (S3 with bucket-level access blocked, GCS
223+
with signed-blob URLs, a custom storage that returns a tokenised
224+
download path, …). The URL is time-bound and per-request; an
225+
unauthenticated reader who captures the URL out-of-band has it for
226+
the signing TTL only. No further consumer action needed beyond
227+
setting the storage's TTL conservatively (matching `SESSION_COOKIE_AGE`
228+
is a reasonable upper bound).
229+
- **Local-storage backends** with `MEDIA_URL` pointing at the same
230+
domain as the admin. The URL is just a path under the consumer's
231+
static-files server. If `MEDIA_URL` is publicly readable (the
232+
default Django development configuration), then any logged-in
233+
staff user who can view the row can also share the file URL with
234+
anyone — including unauthenticated parties — and they can fetch
235+
the file. For production deployments where files should stay
236+
staff-only, put `MEDIA_URL` behind the same staff-gate as the
237+
rest of the admin: `django-private-storage`, an `nginx`
238+
`auth_request` to the Django session check, or a small
239+
Django view that reads `MEDIA_ROOT` and gates on
240+
`request.user.is_active and request.user.is_staff`.
241+
242+
This is the same disclosure surface Django's HTML admin ships with
243+
(its `<a href="{{ field.url }}">` does not check the linked URL's
244+
ACL either). The SPA does not change the posture; it does make the
245+
URLs trivially scriptable against `/api/v1/<app>/<model>/<pk>/`,
246+
which raises the operational stakes of `MEDIA_URL` configuration.
247+
215248
## 10. Cross-references
216249

217250
- [`ACCEPTANCE.md`](ACCEPTANCE.md) §4 — measurable security criteria.

0 commit comments

Comments
 (0)