diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b61494c7b..ad6265ee67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,13 @@ aggregate instead: an italic *Catalog* line at the end of the version section an were served as bare `text/plain`, which a strict client decodes as Latin-1 mojibake. nginx now declares `charset utf-8` on text responses in both server blocks, and the daily bot-serving monitor asserts it on `llms.txt` (#10492). +- **The image bucket is no longer publicly listable** — `allUsers` held + `roles/storage.objectViewer` on `anyplot-images`, whose bundled `storage.objects.list` let + anyone enumerate the whole bucket, including the `staging/` prefix where unreviewed pre-merge + renders sit indistinguishable from promoted ones. Swapped to `roles/storage.legacyObjectReader` + (get-only): every render URL keeps working (verified live incl. CORS), only the directory + listing is gone. Applied 2026-08-19 via gcloud — bucket IAM does not deploy with the repo; the + policy and re-apply procedure are recorded in the project guide (#10493). ### Changed diff --git a/agentic/docs/project-guide.md b/agentic/docs/project-guide.md index 3f640d5196..495ad3a201 100644 --- a/agentic/docs/project-guide.md +++ b/agentic/docs/project-guide.md @@ -395,6 +395,30 @@ page under `COEP: require-corp` must load the renders with a `crossorigin` attribute (or through its own proxy); plain same-tab fetches and `` tags are unaffected. +### Bucket public-access policy + +Objects are publicly fetchable by URL, but the bucket is NOT publicly listable: +`allUsers` holds `roles/storage.legacyObjectReader` (grants `storage.objects.get` +only). It previously held `roles/storage.objectViewer`, whose bundled +`storage.objects.list` let anyone enumerate the whole bucket — including the +`staging/` prefix, where unreviewed pre-merge renders sit indistinguishable +from promoted ones (AI-access audit 2026-08-19; changed 2026-08-19). Like the +CORS policy above, this is bucket metadata that does not deploy with the repo — +re-apply after a bucket rebuild. Both commands are needed: adding the get-only +role does not remove public listability if an `objectViewer` binding exists +(the removal is a no-op when the binding is already absent): + +```bash +gcloud storage buckets add-iam-policy-binding gs://anyplot-images \ + --member=allUsers --role=roles/storage.legacyObjectReader +gcloud storage buckets remove-iam-policy-binding gs://anyplot-images \ + --member=allUsers --role=roles/storage.objectViewer +``` + +Never grant `allUsers` a role that includes `storage.objects.list`. To verify: +an object URL must return 200 anonymously, while +`https://storage.googleapis.com/storage/v1/b/anyplot-images/o` must return 401. + ## Tech Stack - **Backend**: FastAPI, SQLAlchemy (async), PostgreSQL, Python 3.13+