[bug] Any JSON: non-string "value" for unregistered type URL decodes to empty
Maintainers usually reply within 3 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
Research direction
Start in buffa-types/src/any_ext.rs at the unregistered-type-URL fallback around lines 398-404, then read fallback_base64_without_registry and fallback_base64_with_registry_but_unknown_type around lines 858 and 895. Add regression coverage for non-string value shapes while keeping valid base64 behavior covered, and run the focused Any deserialization tests to confirm malformed inputs error without changing valid fallback decoding.
Written by the indexing model from the issue text.
Description
Summary
Any's JSON deserializer silently accepts a value of any JSON type on the unregistered-type-URL path and decodes it to an empty payload, so a malformed document becomes a successfully parsed Any with zero bytes, which then unpacks into an all-default message.
Reproduction
// features: json (serde_json), no AnyRegistry entries
use buffa_types::Any;
let a: Any = serde_json::from_str(
r#"{"@type":"type.example.com/Unknown","value":{"a":1}}"#
).unwrap();
assert_eq!(a.value, Vec::new()); // passes: the object was discarded
value can also be a number, an array, a bool, or null; all five come back as Ok(Any { type_url, value: [] }).
In buffa-types/src/any_ext.rs, the None => match obj.remove("value") arm (around :398) has three cases:
Some(serde_json::Value::String(s))-> base64 decode,:400-403_ => alloc::vec::Vec::new()-> no error,:404
Why this looks unintended
The doc comment on the impl states this path is base64 (any_ext.rs:280-283: "Falls back to base64-encoded value when the registry is absent or the type URL is not registered"), and the serializer it inverts only ever produces a string (:344-349, map.serialize_entry("value", &Base64Bytes(&self.value))). The comment above the branch says what it is for: "Fallback: base64 decode the "value" field." (any_ext.rs:399). Decoding something that is not a string is not a base64 decode that happened to produce zero bytes — it is input the branch has no defined behaviour for.
The same deserialize rejects every other shape of the same mistake, which is why this looks unintended:
| bad input | result | where |
|---|---|---|
"@type": 5 |
Err("@type must be a string") |
:362-365 |
"@type": "NoSlash" |
Err("@type must be a valid type URL ...") |
:377-382 |
"value": "!!!" (string, invalid base64) |
Err from the base64 decoder |
:401-403 |
registered WKT + "value": 123 |
Err(invalid type: integer ...) |
:390-393 |
unregistered + "value": <non-string> |
Ok with value == [] |
:404 |
And the empty payload is the part that survives the round trip: unpack_unchecked / unpack_if on it yields a default Timestamp (1970-01-01T00:00:00Z) or a zero Duration with no error, re-serializing gives {"@type":"...","value":""}, and the input document's actual content is gone with nothing recorded. So a transport that admits a truncated or double-encoded message reports a valid-looking default instead of failing — the value is wrong, but no error surfaces anywhere in the call stack.
Suggested fix
Error on the non-string case instead of defaulting, which makes the fallback branch mean exactly what its comment says (keeping an absent value as an empty payload is a separate call, and Some(Value::Null) probably belongs with "absent"):
None => match obj.remove("value") {
Some(serde_json::Value::String(s)) => { /* base64 decode, as now */ }
None | Some(serde_json::Value::Null) => alloc::vec::Vec::new(),
Some(_) => return Err(serde::de::Error::custom(
"Any: \"value\" for an unregistered type must be a base64 string")),
},
Roughly four lines plus a regression test asserting is_err() for number / object / array / bool and is_ok() for the valid base64 case, so the test cannot be satisfied by rejecting everything. That is small enough to send as a PR if you agree with the direction — happy to, and I would rather confirm the contract first because it turns currently-Ok documents into Err.
Context
I noticed this while reading buffa-types for the None arm's asymmetry against the registered branches. fallback_base64_without_registry (:858) and fallback_base64_with_registry_but_unknown_type (:895) cover valid base64 strings only, and I found no test asserting the non-string case, so I am reading current behaviour as unintended rather than pinned.
I also saw that #483 rewrites the beginning of this same deserialize to read keys through BufferedObject. This is a different thing — it is about what the value may be, not how keys arrive — so I do not think the two conflict in intent, but I would rebase on that if it lands first.
- Dominant language
- Rust
- Stars
- 927
- Forks
- 119
- Avg merge
- 3d 6h
- Merged PRs (30d)
- 152
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from anthropics/buffa
-
codegen: `generate` does not check `CodeGenConfig::extern_paths`, so a direct caller's malformed proto name is kept and never matches a typePossibly taken @vladislav-miroshnikov claimed this 13 days ago. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
anthropics/buffa#703 ·
Maintainers usually reply within 3 days
-
types: `AnyView` has none of `Any`'s type-URL helpersPossibly taken @farmer-data claimed this 12 days ago. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
anthropics/buffa#643 ·
Maintainers usually reply within 3 days
-
docs: `DescriptorPool::files()` does not say that link-time inference is left out of the returned descriptorsPossibly taken @veljjanoski claimed this 12 days ago. Opendocumentation
Difficulty 1/5 Under an hour Newbie friendliness 78/100
anthropics/buffa#642 · 1 comment ·
Maintainers usually reply within 3 days
-
descriptor: an editions file whose edition the pool does not define links with the edition 2023 defaultsPossibly taken @saloni-garg claimed this 4 days ago. Openquestion
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
anthropics/buffa#634 ·
Maintainers usually reply within 3 days
-
codegen: the generated `TYPE_URL` doc names `Any::pack` / `Any::unpack_if`, not `pack_message` / `unpack_message`Possibly taken @veljjanoski claimed this 12 days ago. Opendocumentation
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
anthropics/buffa#621 · 1 comment ·
Maintainers usually reply within 3 days
All issues in anthropics/buffa
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
memory_patch_page drops the `#` of headings like `## Notes on C#`Possibly taken @viniciusdsandrade claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
akitaonrails/ai-memory#1859 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
wealthfolio/wealthfolio#2376 ·
Maintainers usually reply within 1 day
-
kind/bug kind/security
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
spiceai/spiceai#15826 · 1 comment ·
Maintainers usually reply within 1 day
-
area:cli bug dotnet good first issue priority:medium
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day