download_attachment emits unpadded base64 while documenting standard alphabet, so strict decoders reject it #116
Labels
No labels
blocked
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
waiting-on-julian
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/matrix-mcp#116
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
DownloadAttachmentResult.body_base64is documented as "Base64-encoded(standard alphabet, no line breaks) file contents". It is emitted without
padding, and standard-alphabet base64 is padded. A strict decoder rejects it.
Observed against
v0.10.3(sha256:81e26f67…), fetching a real 1273922-bytePNG from an E2EE room:
base64.b64decode(b64 + '=' * (-len(b64) % 4))then decodes to exactly 1273922bytes, matching
size_bytes, with a valid PNG signature and anae 42 60 82IEND CRC at the tail. So the payload is correct and complete; only the padding
is missing.
Python's
base64.b64decodeis strict by default, Rust'sbase64engine withSTANDARDis strict, and Go'sbase64.StdEncodingis strict. Any of them failson this. Node's
Buffer.from(s, 'base64')andatobtolerate it, which is whyit can go unnoticed: the first client to try it may be one of the lenient ones.
The failure is also confusing rather than obvious.
content_type,size_bytesand
filenameall come back correct, so a caller sees a well-formed result and adecoder error, and the natural conclusion is that the file is corrupt or still
encrypted — which is precisely the diagnosis this field exists to make possible.
Fix
Either pad the output, or change the doc comment to say unpadded and name the
alphabet precisely (
STANDARD_NO_PAD). Padding it is the better of the two:the field is already documented as standard, callers on strict decoders are the
majority, and the change is backwards-compatible because lenient decoders accept
padded input.
Acceptance
Not "it round-trips in our tests". Decode the field with a strict decoder and
assert the result equals
size_bytes—base64::engine::general_purpose::STANDARDin Rust, or
base64.b64decode(s)with no padding fix-up. Break it by removingone character and watch the assertion fail; a test that pads before decoding
cannot observe this bug at all, which is how it got here.
Provenance
Found while proving #111's acceptance end to end: fetched
$mr1GZEv2zJuHc87Xl-DMmP1VbgP9Jp5KV1ScA9R9-6Efrom!nJqaJVNKzmgkUjjSLE:kampong.socialthrough the/mcpmount onv0.10.3. Thedecrypted image is correct and readable; the padding cost one round of
diagnosis.
Confirmed at source, and the cause I wrote in the body is wrong. Correcting it
here rather than quietly, because the wrong cause implies a different fix.
Verified on
origin/main:src/mcp.rs:5178—base64::engine::general_purpose::STANDARD_NO_PAD.encode(&bytes)src/mcp.rs:2333— the field's doc, twelve lines up: "Base64-encoded(standard alphabet, no line breaks)". It names the alphabet, names the line
breaks, and is silent on padding. A reader takes that as standard base64, and
standard base64 is padded. Code and doc disagree in the one respect that
decides whether a strict decoder accepts the output.
general_purpose::STANDARDappears nowhere insrc/.STANDARD_NO_PADis theonly standard-alphabet engine in the tree, so nothing internal contradicts the
choice, which is part of why it reads as deliberate.
What I claimed: a test that padded before decoding hid the defect.
Actual: there is no decode site anywhere in
src/.git grep '\.decode('over
origin/mainreturns nothing, andbody_base64appears in exactly threeplaces — the field declaration at 2333, the encode at 5178, and the struct
initialiser at 5185. Nothing in this repository has ever decoded the encoder's
output. So it is not a check that could not observe the defect; it is the absence
of any check at all, on a field whose entire purpose is to be decoded by
somebody else.
Therefore the acceptance in the body is too weak, and this replaces it. The
test must decode real
download_attachmentoutput with Rust'sbase64::engine::general_purpose::STANDARDand assert success, and it must failbefore the fix.
STANDARDrejects unpadded input, so that test can fail today,which is the only property that makes it evidence. A test using
STANDARD_NO_PADpasses in both worlds and is worse than no test, because itlooks like coverage of exactly this field.
Found by Clark reading the source after the defect was observed from outside.