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
Check who is asking before handing over a room #258 — nine more read handlers with the same defect: /rooms/{id}/state, /state/{type}/{key}, /messages, /event/{eventId}, /context/{eventId}, three /relations/ spellings, /threads. Four of them bound Authenticated(_identity) and discarded it at the binding site. Confirmed by probe: HTTP 200 with private message bodies to a user in no rooms at all.
The federation surface was stricter than the client surface the whole time — federation_room_origin refuses a remote server with no member in the room. A peer could not read what a local stranger could.
Both were found by a human reading, not by a test, a lint, or a review. That is the thing to fix: the finding rate is currently "however much someone happens to look".
What an audit should cover
Authorization, systematically. Every handler, against a matrix of caller states: stranger, invited, joined, left, banned, server admin, appservice, appservice ghost. The property is not "does it 403" but "what does a caller in state X learn". #258's test is a route table walked by two tests; the same shape should cover every route, and the table should be generated from the router rather than hand-maintained, so a new route is in it by default.
History visibility properly.#258 implements joined-or-world_readable and is knowingly stricter than the spec: a user who has left a room can no longer read the history they are entitled to. That is a deliberate, recorded deviation and it needs finishing — bounding each read at the caller's departure point.
Rate limiting and resource exhaustion.[ratelimit] exists and every test disables it. What is actually limited, and what is not? Unbounded per-user growth was found and capped in one place this week (#264, delayed events) by reading the issue that predicted it — the same question has not been asked of filters, account data, aliases, media, device lists, or one-time keys.
Media.serve_media and thumbnail_media take _identity and discard it: any authenticated user can fetch any MXC by ID. That matches Synapse's authenticated-media model, where possession of the URI is the capability — but it composes badly with a room-read hole, since an attacker who can read a private room's state can harvest the URIs. Worth an explicit decision rather than an inherited one.
Write authorization, not just reads.create_alias lets any authenticated user attach an alias to any existing room — it checks rooms.exists and not membership. may_publish argues the opposite policy for directory visibility three hundred lines away. One of the two is wrong.
The signing and federation trust boundary. Key rotation, server_keys caching and expiry, what happens when a peer presents a key we have not seen, and whether a signature failure is distinguishable from a network failure to the caller.
Secrets in logs and errors.MatrixError::internal deliberately hides detail from the response and logs it — check nothing else does the reverse.
Shape
Not a one-off document. The valuable output is:
A findings list with severity and a reproduction for each, filed as individual issues.
A generated route-coverage table, so "which endpoints have an authorization test" is a number that CI can hold at 100% rather than a question nobody asks.
An external audit is worth having eventually. It is worth much more after the above, because otherwise it spends its budget finding what a route table would have.
This is not hypothetical. In one afternoon, reading code that had passed review and a green CI, two authorization defects were found:
room_subscriptionsentry was never membership-checked. Any registered account could name any room ID and be sent its name and full timeline./rooms/{id}/state,/state/{type}/{key},/messages,/event/{eventId},/context/{eventId}, three/relations/spellings,/threads. Four of them boundAuthenticated(_identity)and discarded it at the binding site. Confirmed by probe: HTTP 200 with private message bodies to a user in no rooms at all.The federation surface was stricter than the client surface the whole time —
federation_room_originrefuses a remote server with no member in the room. A peer could not read what a local stranger could.Both were found by a human reading, not by a test, a lint, or a review. That is the thing to fix: the finding rate is currently "however much someone happens to look".
What an audit should cover
Authorization, systematically. Every handler, against a matrix of caller states: stranger, invited, joined, left, banned, server admin, appservice, appservice ghost. The property is not "does it 403" but "what does a caller in state X learn". #258's test is a route table walked by two tests; the same shape should cover every route, and the table should be generated from the router rather than hand-maintained, so a new route is in it by default.
History visibility properly. #258 implements joined-or-
world_readableand is knowingly stricter than the spec: a user who has left a room can no longer read the history they are entitled to. That is a deliberate, recorded deviation and it needs finishing — bounding each read at the caller's departure point.Rate limiting and resource exhaustion.
[ratelimit]exists and every test disables it. What is actually limited, and what is not? Unbounded per-user growth was found and capped in one place this week (#264, delayed events) by reading the issue that predicted it — the same question has not been asked of filters, account data, aliases, media, device lists, or one-time keys.Media.
serve_mediaandthumbnail_mediatake_identityand discard it: any authenticated user can fetch any MXC by ID. That matches Synapse's authenticated-media model, where possession of the URI is the capability — but it composes badly with a room-read hole, since an attacker who can read a private room's state can harvest the URIs. Worth an explicit decision rather than an inherited one.Write authorization, not just reads.
create_aliaslets any authenticated user attach an alias to any existing room — it checksrooms.existsand not membership.may_publishargues the opposite policy for directory visibility three hundred lines away. One of the two is wrong.The signing and federation trust boundary. Key rotation,
server_keyscaching and expiry, what happens when a peer presents a key we have not seen, and whether a signature failure is distinguishable from a network failure to the caller.Secrets in logs and errors.
MatrixError::internaldeliberately hides detail from the response and logs it — check nothing else does the reverse.Shape
Not a one-off document. The valuable output is:
An external audit is worth having eventually. It is worth much more after the above, because otherwise it spends its budget finding what a route table would have.
Related