Repository navigation
security: validate device command payloads against the sealed hierarchy - #14
Open
IbraTech04 wants to merge 1 commit into
Open
IbraTech04 wants to merge 1 commit into
IbraTech04 wants to merge 1 commit into
Conversation
IssueCommandRequest.payload() is a raw JsonNode. The dispatcher
stringified it and handed it to the device verbatim, so the sealed
CommandPayload records were never deserialized on the only path that
issues commands -- LogsTailPayload's 1..500 bound, and every other
constraint in that hierarchy, was dead code. CommandPayload's own
javadoc admitted it. An operator holding board:command:inspect could
send {"kind":"logs.tail","payload":{"lines":999999999}} and it reached
the kiosk unvalidated, with every bound delegated to agent code that
does not live in this repository and cannot be audited from here.
New CommandPayloadCodec binds the wire JSON to the subtype selected by
the command kind, rejects a payload whose discriminator contradicts
that kind, runs bean validation, and rejects unknown properties via a
dedicated reader -- the shared ObjectMapper has FAIL_ON_UNKNOWN_PROPERTIES
off, which is right for lenient DTOs and wrong for something bound for
a physical device. The dispatcher stores and forwards the re-serialized
validated object rather than what the client sent, so anything tacked
on beyond the record's components is dropped.
Not exploitable as RCE from what is visible -- every payload record is
zero-arg except LogsTailPayload.lines -- but the validation looked
authoritative while doing nothing, which is how it becomes a real hole
the first time a payload field carries a path or a URL.
The sealed interface and its closed Id.NAME subtype registry are
unchanged; that anti-gadget property is deliberate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xQusSt39vP3c82gAUXRCm
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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.
The problem
IssueCommandRequest.payload()is a rawJsonNode. The dispatcher stringified it and handed it to the device verbatim — the sealedCommandPayloadhierarchy was never deserialized on the only path that issues commands.CommandPayload's own javadoc admitted it:So
LogsTailPayload's@Min(1) @Max(500)bound — and every other constraint in that hierarchy — was dead code that merely looked authoritative. An operator holdingboard:command:inspectcould send{"kind":"logs.tail","payload":{"lines":999999999,"anything":"..."}}and it reached the physical kiosk unvalidated, with every bound delegated to agent code that does not live in this repository and cannot be audited from here.This is not RCE from what is visible — every payload record is zero-arg except
LogsTailPayload.lines. It is flagged because validation that exists and does nothing is how this becomes a real hole the first time someone adds a payload field carrying a path or a URL.The fix
CommandPayloadCodecbinds the wire JSON to the subtype selected by the command kind, then:kind;ObjectReader— the sharedObjectMapperhasFAIL_ON_UNKNOWN_PROPERTIESoff, which is the right default for lenient DTOs and exactly wrong for something bound for a physical device.The dispatcher now stores and forwards the re-serialized validated object, not what the client sent, so anything tacked on beyond the record's components is dropped rather than relayed.
The sealed interface and its closed
Id.NAMEsubtype registry are unchanged — that anti-gadget property is deliberate and stays.Verification
Covers the exact audit payload (
lines: 999999999) now rejected withlines must be between 1 and 500, both boundaries (0/501 rejected, 1/500 accepted), unknown properties, a contradicting discriminator, non-object payloads, and that zero-arg commands still accept absent/empty/null payloads.Verified by test rather than live curl: issuing a real command needs a connected Pi agent that does not exist on this machine, and the dev DB has no enrolled devices.
Preserved
onAck/onProgress/onResultstill scope every inbound frame tosession.getDeviceId()vialoadOwned, so a rogue agent cannot forge results for another device's commands.CommandAuthorizer's risk-level gating is untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_011xQusSt39vP3c82gAUXRCm