An upload is claimed against the message sender, so it must go out under the bound bot (comms-qpup) - #219
Draft
GraemeF wants to merge 1 commit into
Draft
An upload is claimed against the message sender, so it must go out under the bound bot (comms-qpup)#219GraemeF wants to merge 1 commit into
GraemeF wants to merge 1 commit into
Conversation
…der the bound bot (comms-qpup) `uploadFile` posted `/api/v1/user_uploads` through `minterHttp` while `post` went out through `boundHttp()`. Zulip's `do_claim_attachments` validates each attachment against the MESSAGE SENDER, so the claim was skipped, the row that grants read access was never written, and the message sent anyway. The link rendered and nobody could open it — including the realm owner. `uploadFile` now runs through `boundHttp()`, which put it on the mint seam with the other attribution-producing verbs and pulled four things after it: - `BotHttp` had no `uploadRaw`, so the bound client could not upload at all. Widened by that one member, passed through `wrapBotHttp` untouched. The wall that wrapper enforces is a recipient rule on `POST /messages`, and an upload addresses no recipient. `bot-dm-guard.test.ts` now asserts the same wrapped client that just uploaded still refuses a bot-to-bot direct message. - `upload_file` became a binding tool, so it takes the host-supplied `session_id` and joins the PreToolUse matcher. Without it a seat whose first commy call is an upload gets `UnboundEphemeralSession` instead of a file. - `hooks-manifest.test.ts` traced a tool's bound path through `adapter.publisher.*` and `adapter.inbox.*` only. `upload_file` reaches the adapter through a dep closure, so the guard would have reported coverage it did not have. It now resolves the dep alias and covers the attachment receiver too. - A live test in `realm.live.test.ts` is the proof: bot A uploads and posts a four-byte file; a second freshly-minted bot — owns nothing, subscribed to nothing — reads it back. Measured 403 before the change, bytes after. The reader is a second bot rather than `adapter.downloadFile` because `downloadFile` reads through the minter, and the minter is subscribed to every channel on this realm. Its read succeeds on a ground this change does not touch, so it cannot tell a claimed attachment from an unclaimed one. `downloadFile` stays on `minterHttp`, so a seat that only ever reads still never binds. That now rests on two pieces of realm state this repo does not control — no invite-only channels, and the minter's blanket subscription — and the call site records both, says plainly that neither is established by a discriminating test, and names what would discriminate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes
comms-qpup.What was broken
uploadFileposted/api/v1/user_uploadsthroughminterHttp.postwent outthrough
boundHttp(). So the account that owned the file and the account thatsent the message referencing it were different accounts.
Zulip's
do_claim_attachmentsvalidates each attachment against the messagesender, not the uploader and not the reader. On a mismatch it logs a warning
and
continues. The message sends, the markdown link renders, and the row thatgrants read access is never written. Every reader then fails all three tests in
validate_attachment_request, so the link is dead for everyone — including therealm owner, who gets a 403 on his own realm.
Reported by
cc-homelab-cbb5b9c3with realm evidence: 11 commy uploads, allorphaned; 13 web-client uploads, none orphaned.
The fix, and what it pulled after it
uploadFileruns throughboundHttp(). That put it on the mint seam with theother attribution-producing verbs, which took four more changes:
BotHttphad nouploadRaw. It wasPick<ZulipHttp, 'get' | 'post' | 'patch' | 'delete'>, so the bound client could not upload at all. Widened bythat one member and passed through
wrapBotHttpuntouched.Widening a security-adjacent type is the move that gets accepted once and cited
for the next member, so the reasoning is pinned in the code rather than left
here: the wall that wrapper enforces is a recipient rule on
POST /messages,and an upload addresses no recipient.
bot-dm-guard.test.tsnow asserts thesame wrapped client that just uploaded still refuses a bot-to-bot DM.
upload_filebecame a binding tool. It takes the host-suppliedsession_id, gainedcwd, and joined the PreToolUse matcher. Without this aseat whose first commy call is an upload gets
UnboundEphemeralSessioninsteadof a file —
ensureBoundFor(undefined, …)returns the unbound stub.The guard test could not see it.
hooks-manifest.test.tstraced a tool'sbound path through
adapter.publisher.*andadapter.inbox.*only.upload_filereaches the adapter through a dep closureserver.tshands it, sothe rule "every tool whose adapter path reaches
boundHttpreceivessession_id" would have stayed green while being blind to the newest member —the
comms-65njfailure that file's own header names. The trace now resolvesthe
const upload = deps.uploadalias and covers the attachment receiver.A live test is the proof. Bot A uploads a four-byte file and posts it. A
second freshly-minted bot — owns nothing, subscribed to nothing — reads it back
through its own credential.
Measured, both ways
uploadFileonminterHttp(before)boundHttp()(after)The reader is a second bot rather than
adapter.downloadFileon purpose.downloadFilereads through the minter, and the minter is subscribed to everychannel on this realm, so its read succeeds on a ground this change does not
touch. That instrument cannot tell a claimed attachment from an unclaimed one. A
fresh bot fails the ownership test and the
UserMessagetest, leavingis_realm_publicas its only route — and that flag is stamped onto theAttachmentrow only when the claim succeeds.The before-side run minted one orphaned attachment by design. It was deleted;
the realm's orphan count is unchanged.
What
downloadFilerests on nowIt stays on
minterHttp, so a seat that only ever reads still never binds. Withthe minter no longer owning the files it reads, that rests on two pieces of
realm state this repo does not control:
is_realm_publicis stamped at claim time.UserMessagefor thereferencing message.
Neither is established by a discriminating test. They overlap completely on
this realm, so no download run against it can say which one admitted the reader.
What would discriminate: an invite-only channel the minter is not subscribed to,
carrying an attachment posted by another account. Building that mutates the
realm, so it is the operator's call. The call-site comment records all of this,
and names the fix if either ground goes away.
Out of scope
comms-l5ci, blocked on this.One thing to fix separately
ZULIP_LIVE_CHANNEL_NAME=agent-comms-testnames a channel that does not existon the realm — 1204 streams, no match. Every channel-gated live test fails with
UnknownChannelon plainmain, including the pre-existing resolve-then-postsuite. This is not caused by this branch. The measurements above were taken with
ZULIP_LIVE_CHANNEL_NAME=test(stream id 4, public).