Skip to content

fix(cluster): Reject truncated command and attribute report payloads - #193

Open
RobinBol wants to merge 2 commits into
masterfrom
fix/malformed-frame-hardening
Open

RobinBol wants to merge 2 commits into
masterfrom
fix/malformed-frame-hardening

Conversation

@RobinBol

@RobinBol RobinBol commented May 20, 2026 •

Copy link
Copy Markdown
Collaborator

What

A command or attribute report whose payload is too short for its fields is now rejected with a MALFORMED_COMMAND Default Response, and its handler or attr.<name> event does not fire.

Why

@athombv/data-types zero-fills missing bytes instead of failing. A truncated IAS Zone zoneStatusChangeNotification or zoneStatus report therefore reached drivers as "all alarms clear", and a truncated status field as SUCCESS. A driver cannot tell that apart from a real report.

How

  • lib/util/parseCommandArgs.js, used by Cluster.handleFrame and BoundCluster.handleFrame, checks the payload against the fixed size of the command's args before parsing, and turns a parser throw into MALFORMED_COMMAND.
  • Commands with encodeMissingFieldsBehavior: 'skip' (OTA) may omit trailing fields, so for those only the first field is required. Every such command starts with a discriminator byte.
  • ZCLAttributeDataRecord checks that a fixed-width attribute value fits in the remaining bytes.

Anything Else

Variable-length fields (octstr and similar) are not covered: bufferFromBuf returns a partial buffer without throwing. The real fix belongs in @athombv/data-types, where short reads should fail instead of returning zeros; two related hardening changes landed in athombv/node-data-types#44.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens inbound ZCL command argument parsing so truncated/unparseable command payloads are rejected with ZCLError('MALFORMED_COMMAND') instead of being silently parsed (often as all-zero “SUCCESS”/“all-clear” values). This leverages existing Endpoint.handleFrame behavior to automatically emit a proper ZCL Default Response without additional wiring.

Changes:

  • Added a shared parseCommandArgs() helper that performs a minimum-length precheck (when applicable) and converts parse-time exceptions into MALFORMED_COMMAND.
  • Updated Cluster.handleFrame and BoundCluster.handleFrame to use the new helper instead of calling command.args.fromBuffer(...) directly.
  • Added tests to ensure truncated IAS Zone frames don’t invoke handlers and do produce MALFORMED_COMMAND Default Responses (with regressions for well-formed frames).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
test/testMalformedFrames.js Adds regression + hardening tests verifying truncated payloads yield MALFORMED_COMMAND and do not call handlers.
lib/util/parseCommandArgs.js Introduces defensive argument parsing helper with length precheck + parse error normalization.
lib/Cluster.js Routes inbound cluster command arg parsing through parseCommandArgs().
lib/BoundCluster.js Routes inbound bound-cluster command arg parsing through parseCommandArgs().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/Cluster.js Resolved
Comment thread test/testMalformedFrames.js Resolved
RobinBol added a commit that referenced this pull request May 21, 2026
Follow-up to the command-arg hardening. Addresses two bypasses that
remained after the first pass:

1. Attribute reports (cmdId 0x0A reportAttributes) parse a raw payload
   into ZCLAttributeDataRecord entries, where each entry's value field
   was read via `DataType.fromBuffer` without bounds checking. For
   fixed-width types like map16 (IAS Zone `zoneStatus`, attribute id
   0x0002), a missing or short value silently produced an all-bits-false
   bitmap that propagates to drivers as "all alarms clear". Same fail-
   open path as the command-arg case from PR #193, just routed through
   the attribute-report channel.

   Add a length precheck in `ZCLAttributeDataRecord.fromBuf` before
   calling the value's `fromBuffer`. On short input, throw
   `ZCLError('MALFORMED_COMMAND')` which `Endpoint.handleFrame` already
   converts to a proper default-response back to the sender.

2. The previous `encodeMissingFieldsBehavior: 'skip'` exemption in
   `parseCommandArgs` was too broad: it skipped *all* length checking
   for those commands, so an empty payload to e.g. OTA
   `queryNextImageRequest` parsed as all-zero fields. Tighten to require
   at least the first field's byte width, which preserves the legitimate
   "omit trailing optional fields" case while rejecting truncated
   leading-discriminator payloads.

5 new tests, including regression guards for well-formed reports and
for OTA's omit-trailing-fields path. 86/86 tests pass (was 81 + 5).
RobinBol added a commit that referenced this pull request Oct 23, 2026
Follow-up to the command-arg hardening. Addresses two bypasses that
remained after the first pass:

1. Attribute reports (cmdId 0x0A reportAttributes) parse a raw payload
   into ZCLAttributeDataRecord entries, where each entry's value field
   was read via `DataType.fromBuffer` without bounds checking. For
   fixed-width types like map16 (IAS Zone `zoneStatus`, attribute id
   0x0002), a missing or short value silently produced an all-bits-false
   bitmap that propagates to drivers as "all alarms clear". Same fail-
   open path as the command-arg case from PR #193, just routed through
   the attribute-report channel.

   Add a length precheck in `ZCLAttributeDataRecord.fromBuf` before
   calling the value's `fromBuffer`. On short input, throw
   `ZCLError('MALFORMED_COMMAND')` which `Endpoint.handleFrame` already
   converts to a proper default-response back to the sender.

2. The previous `encodeMissingFieldsBehavior: 'skip'` exemption in
   `parseCommandArgs` was too broad: it skipped *all* length checking
   for those commands, so an empty payload to e.g. OTA
   `queryNextImageRequest` parsed as all-zero fields. Tighten to require
   at least the first field's byte width, which preserves the legitimate
   "omit trailing optional fields" case while rejecting truncated
   leading-discriminator payloads.

5 new tests, including regression guards for well-formed reports and
for OTA's omit-trailing-fields path.
@RobinBol
RobinBol force-pushed the fix/malformed-frame-hardening branch from e50f2ea to cfaaec0 Compare October 23, 2026 08:27
@RobinBol
RobinBol requested a balanced review from Copilot October 23, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Shared parsing changes affect all clusters, and conditional payload compatibility remains unresolved and requires human review.

2 open issues
2 resolved since last review

Review effort: Balanced

// Fixed-width field's length is a positive integer; varsize is negative or 0.
// For varsize first fields (uncommon for 'skip' commands), require at least
// 1 byte so the length prefix is present.
return firstField.length > 0 ? firstField.length : 1;
// > 0 : exact byte count for an all-fixed-size struct
// < 0 : negative of the fixed-portion size for a varsize struct
// = 0 : no payload required
return Math.abs(argsType.length);
@RobinBol RobinBol added the fix Bug fix, release notes: Fixes label Oct 23, 2026
The parser primitives in @athombv/data-types silently substitute zero
values when the source buffer is shorter than the declared field length.
For a ZCL command struct, this means a truncated payload parses into an
object where every numeric field is 0 and every bitmap field has all
bits clear - which for alarm-routing clusters (IAS Zone) happens to look
like "all clear" and for status-bearing response frames happens to look
like "SUCCESS".

This change adds a length precheck and try/catch around command arg
parsing in Cluster.handleFrame and BoundCluster.handleFrame. Truncated
payloads now throw ZCLError('MALFORMED_COMMAND'), which Endpoint.handleFrame
already converts into a proper default-response back to the sender.

Commands declared with encodeMissingFieldsBehavior: 'skip' (OTA) are
exempt from the strict length precheck because they intentionally allow
trailing fields to be omitted; for those, only the try/catch fallback
applies.
Follow-up to the command-arg hardening. Addresses two bypasses that
remained after the first pass:

1. Attribute reports (cmdId 0x0A reportAttributes) parse a raw payload
   into ZCLAttributeDataRecord entries, where each entry's value field
   was read via `DataType.fromBuffer` without bounds checking. For
   fixed-width types like map16 (IAS Zone `zoneStatus`, attribute id
   0x0002), a missing or short value silently produced an all-bits-false
   bitmap that propagates to drivers as "all alarms clear". Same fail-
   open path as the command-arg case from PR #193, just routed through
   the attribute-report channel.

   Add a length precheck in `ZCLAttributeDataRecord.fromBuf` before
   calling the value's `fromBuffer`. On short input, throw
   `ZCLError('MALFORMED_COMMAND')` which `Endpoint.handleFrame` already
   converts to a proper default-response back to the sender.

2. The previous `encodeMissingFieldsBehavior: 'skip'` exemption in
   `parseCommandArgs` was too broad: it skipped *all* length checking
   for those commands, so an empty payload to e.g. OTA
   `queryNextImageRequest` parsed as all-zero fields. Tighten to require
   at least the first field's byte width, which preserves the legitimate
   "omit trailing optional fields" case while rejecting truncated
   leading-discriminator payloads.

The tests run on the virtual device; iasZone.js and ota.js cover the
well-formed frames.
@RobinBol
RobinBol force-pushed the fix/malformed-frame-hardening branch from cfaaec0 to 6d00128 Compare October 27, 2026 14:50
@RobinBol RobinBol changed the title fix(cluster): reject truncated command payloads with MALFORMED_COMMAND fix(cluster): Reject truncated command and attribute report payloads Oct 27, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix, release notes: Fixes

Development

Successfully merging this pull request may close these issues.

3 participants