From 4df0671e1733c22614bdcee23e3c24937613d1e5 Mon Sep 17 00:00:00 2001 From: Frankie Chow Date: Fri, 21 Aug 2026 07:23:47 +0100 Subject: [PATCH] fix(services): decode GetEventInformation without object identifier The lastReceivedObjectIdentifier of a GetEventInformation request is optional (ASHRAE 135, 13.12.1.1). A client requesting the first batch of events omits it, which results in a service request without any payload, but the decoder read the identifier unconditionally and therefore ran past the end of the buffer. The resulting RangeError is raised inside the transport message handler, where application code cannot catch it, so a single such request terminates the process of any device using this library to serve BACnet requests. Decode the identifier only when it is actually present. --- lib/services/get-event-information.js | 21 +++++++++++++------ .../service-get-event-information.spec.js | 18 ++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/lib/services/get-event-information.js b/lib/services/get-event-information.js index 09afbb4c..dbfbd2bf 100644 --- a/lib/services/get-event-information.js +++ b/lib/services/get-event-information.js @@ -7,14 +7,23 @@ module.exports.encode = (buffer, lastReceivedObjectId) => { baAsn1.encodeContextObjectId(buffer, 0, lastReceivedObjectId.type, lastReceivedObjectId.instance); }; -module.exports.decode = (buffer, offset) => { +module.exports.decode = (buffer, offset, apduLen) => { let len = 0; const value = {}; - const result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); - len += result.len; - const decodedValue = baAsn1.decodeObjectId(buffer, offset + len); - len += decodedValue.len; - value.lastReceivedObjectId = {type: decodedValue.objectType, instance: decodedValue.instance}; + value.lastReceivedObjectId = null; + // The 'lastReceivedObjectIdentifier' is optional (ASHRAE 135, 13.12.1.1). A + // client asking for the first batch of events omits it, which results in a + // service request without any payload. + const hasObjectId = (apduLen === undefined || apduLen > 0) && + offset < buffer.length && + baAsn1.decodeIsContextTag(buffer, offset, 0); + if (hasObjectId) { + const result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + const decodedValue = baAsn1.decodeObjectId(buffer, offset + len); + len += decodedValue.len; + value.lastReceivedObjectId = {type: decodedValue.objectType, instance: decodedValue.instance}; + } value.len = len; return value; }; diff --git a/test/unit/service-get-event-information.spec.js b/test/unit/service-get-event-information.spec.js index 25db0372..45bffd26 100644 --- a/test/unit/service-get-event-information.spec.js +++ b/test/unit/service-get-event-information.spec.js @@ -14,6 +14,24 @@ describe('bacstack - Services layer GetEventInformation unit', () => { lastReceivedObjectId: {type: 8, instance: 15} }); }); + + it('should successfully decode a request without the optional object identifier', () => { + const buffer = utils.getBuffer(); + const result = baServices.getEventInformation.decode(buffer.buffer, 0, 0); + delete result.len; + expect(result).toEqual({ + lastReceivedObjectId: null + }); + }); + + it('should not read beyond the end of a request without payload', () => { + const buffer = Buffer.from([]); + const result = baServices.getEventInformation.decode(buffer, 0, 0); + delete result.len; + expect(result).toEqual({ + lastReceivedObjectId: null + }); + }); }); describe('GetEventInformationAcknowledge', () => {