:bug: eventing: in multi-tenant mode, group-member push suppression is defeated by omitting ce_userkey
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 70/100
Línea de trabajo
Start in eventbridge/ntfy.py at _store_for(event) and follow how StoreRegistry.for_event picks a store from ce_userkey; then read owner_index.py (OwnerIndex.owner_of) and how owners= is threaded through handlers(...) in __main__.py to mirror that pattern with an owners= parameter on the publisher. Done means a member frame with ce_userkey omitted routes to the real owner's store and is suppressed in multi-tenant mode, while the existing test test_a_member_event_that_only_claims_a_group_is_not_trusted_as_a_member still passes. Add the described stores= passthrough on the test fixture's RecordingNtfy and the multi-store suppression test.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
From the review of #903 (@aslom's comment on eventbridge/ntfy.py:222).
#903 closed the single-tenant omission forgery for group-member push suppression (#891): membership now resolves from group_members via group_of(correlationid) instead of trusting the event's ce_groupid. But which store that table is looked up in is still chosen by an attacker-supplied attribute — in multi-tenant mode, ce_userkey.
The hole
__main__.py wires the publisher as NtfyPublisher(..., stores=stores if topics.multi else None), so with EB_TENANCY_MODE=multi, _store_for(event) resolves through StoreRegistry.for_event(event), which reads getter(ce.EXT_USERKEY) — and a missing userkey is an accepted state there, not a rejected one: it routes to shared/ and increments unattributed.
So a frame that keeps ce_groupid and omits ce_userkey:
for_event→ theshared/store (_multi and not is_valid_userkey→for_userkey(None));group_of(corr)onshared/→None, because the genuine membership row was written tousers/<userkey>/bysubmit_members;groupid is None→return False→ not suppressed.
That reproduces #891's impact exactly: a forged terminal phase=error frame pages at priority 5 with NTFY_GROUP_NOTIFY_ERRORS off, and kafka_in.py's _on_member_event gate (d.get("groupid")) is passed so nothing recounts the member either — the batch arithmetic disagrees with the page, same as before.
Reachable on the same terms as #891: signing.py — "absent attributes are omitted rather than written empty" — so a holder of any key the keyset trusts signs an event that never carried userkey, and the signature verifies.
Not a regression from #903: the pre-change code called self._store_for(event) too, so multi-tenant was equally bypassable before. #903 closes it completely for single-tenant, which is most deployments.
The fix (from the review)
Resolve the store from the ownership index instead of the event: OwnerIndex.owner_of(correlationid) -> (userkey, known) is the global correlationid → userkey index (owner_index.py, DESIGN_PHASE3 §2.6/§6.2), written by Minter at mint time — genuinely not attacker-supplied — and seeded from disk at startup. owners is already threaded to handlers(..., owners=owners if topics.multi else None), so an owners= parameter on the publisher follows the established pattern. With known=False meaning "never minted here", the unrecorded-claimant case still notifies — the property test_a_member_event_that_only_claims_a_group_is_not_trusted_as_a_member pins.
Tests to come with it: a stores= passthrough on the test fixture's RecordingNtfy, and the multi-store case from the review (a member frame with ce_userkey deleted must not buy a push). Until then, the docstring in ntfy.py is scoped to single-tenant with a pointer here.
- Lenguaje dominante
- Python
- Estrellas
- 11
- Forks
- 42
- Merge medio
- 22 h 22 min
- PR fusionados (30 d)
- 20
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de rossoctl/examples
-
stale
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
rossoctl/examples#828 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 34/100
rossoctl/examples#1011 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 35/100
rossoctl/examples#1010 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 25/100
rossoctl/examples#986 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 25/100
rossoctl/examples#985 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de rossoctl/examples
Issues similares
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
521xueweihan/HelloGitHub#4150 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
aws-samples/sample-per-model-bedrock#595 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
Los mantenedores suelen responder en 1 día
-
bug connectors
Dificultad 1/5 Menos de una hora Aptitud para principiantes 67/100
pyinfra-dev/pyinfra#2036 ·
Los mantenedores suelen responder en 9 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día