Security and Memory Safety¶
bac-py implements defense-in-depth for protocol parsing, transport security, memory management, and logging. This guide describes the safety measures built into the library and recommendations for production deployments.
Protocol Safety¶
BACnet uses ASN.1 Basic Encoding Rules (BER) with tag-length-value (TLV) encoding. Malformed or malicious packets can attempt to cause buffer overreads, excessive memory allocation, or deep recursion. bac-py validates all fields before processing:
- Tag and length validation
Every
decode_tag()call validates that the buffer contains enough bytes for the tag number, length field, and content before reading. Truncated packets raiseValueErrorimmediately rather than reading past the buffer boundary. Context-tag extraction (extract_context_value) additionally validates that primitive tag lengths do not extend past the buffer end.- Primitive type buffer checks
decode_real()anddecode_double()validate the input buffer is at least 4 or 8 bytes before callingstruct.unpack_from(), raising a clearValueErrorinstead of an opaquestruct.erroron truncated data. ErrorPDU decoding performs bounds checks after eachdecode_tag()call to reject truncated error class and error code fields.- Allocation caps
Tag lengths exceeding 1 MB (1,048,576 bytes) are rejected to prevent memory exhaustion from malformed length fields. This catches both corrupted packets and deliberate attempts to trigger large allocations.
- Service decoder list caps
All service decode loops (ReadPropertyMultiple, WritePropertyMultiple, alarm summary, COV, write group, virtual terminal, object management, and audit services) enforce a maximum of 10,000 decoded items per message. This prevents crafted payloads with thousands of repeated elements from consuming excessive memory during decoding.
- Context nesting depth
Nested context tags are limited to a depth of 32. Deeply nested or recursive structures that exceed this limit raise
ValueError, preventing stack exhaustion from crafted payloads. This is enforced in the core tag decoder, the audit service decode paths, COV property value decoding, and all service decoders with manual nesting loops.- Segmentation reassembly cap
SegmentReceivertracks cumulative reassembly size and aborts the transaction when the total exceeds 1 MiB (1,048,576 bytes), preventing a peer from consuming unbounded memory with many small segments.- Ethernet frame validation
The Ethernet transport rejects 802.3 frames whose length field is below the minimum LLC header size (3 bytes), preventing underflow when extracting the NPDU payload.
- APDU size constraints
Maximum APDU sizes are enforced per Clause 20.1.2.5 (50–1476 bytes). When communicating with a remote device, the library constrains requests to the minimum of the local and remote device’s
max_apdu_length_acceptedvalue, populated automatically from I-Am responses.
Transport Security¶
- TLS 1.3 for BACnet/SC
BACnet Secure Connect (Annex AB) requires TLS 1.3 with mutual authentication. Both hub and node present X.509 certificates, and the server context sets
ssl.CERT_REQUIREDto enforce client certificate verification. The minimum TLS version is pinned to TLS 1.3 – older protocol versions are not negotiated.- Plaintext warnings
When
allow_plaintext=Trueis set on a TLS configuration (intended only for development and testing), the library logs aWARNINGon every affected path: TLS client context creation, TLS server context creation, and transport startup. These warnings make it immediately visible when encryption is disabled.- Stress test TLS coverage
All BACnet/SC stress tests and benchmarks (both local and Docker) exercise mutual TLS 1.3 by default using a mock CA with EC P-256 certificates. This ensures that TLS handshake overhead and encrypted data paths are included in performance measurements. The local benchmark accepts
--no-tlsto fall back to plaintext for comparison.- VMAC origin validation
The hub function validates the source VMAC address on every received BVLC-SC message against the connection’s registered VMAC. This prevents a connected node from spoofing another node’s address in hub-routed traffic.
- WebSocket frame size limits
SC WebSocket connections support a configurable
max_frame_sizeparameter. Frames exceeding the limit are logged and discarded. After 3 consecutive oversized frames, the connection is closed to prevent log flooding from misbehaving peers. The internal pending events buffer is capped at 64 entries to bound memory when a single TCP segment delivers many WebSocket frames.- VMAC collision atomicity
The hub function reserves a VMAC address in a
_pending_vmacsdictionary when the collision check passes during the BACnet/SC handshake. This prevents a time-of-check/time-of-use (TOCTOU) race where two connections with the same VMAC could both pass the check before either is registered. Reservations expire after 30 seconds and the pending set is capped atmax_connectionsto prevent growth from abandoned handshakes.- URI scheme validation
When a hub provides peer WebSocket URIs via Address-Resolution-ACK, the node switch validates that each URI uses a
ws://orwss://scheme before connecting. URIs with other schemes are logged and skipped, preventing a malicious hub from redirecting the node to non-WebSocket endpoints.- SC header options count and size limits
BVLC-SC header option decoding enforces a maximum of 32 options per list and a maximum of 512 bytes per individual option data field. The BACnet/SC spec defines only two option types (Secure Path and Proprietary), so legitimate messages never approach these limits. This prevents crafted payloads from causing excessive allocations via option count or oversized option data (up to 65,535 bytes per the wire format).
- SC address resolution URI cap
AddressResolutionAckPayloaddecoding truncates the URI list to 16 entries, preventing unbounded allocations from malformed address resolution responses.- Credential redaction
The
SCTLSConfig.__repr__()method redactsprivate_key_pathas'<REDACTED>'so private key paths never appear in logs, tracebacks, or debug output.
See BACnet Secure Connect for TLS certificate setup, hub configuration, and failover.
Logging Safety¶
- Lazy formatting
All log statements use
%s-style placeholder formatting (e.g.,logger.debug("decode APDU type=%s", pdu_type)), not f-strings. This ensures format arguments are only evaluated when the log level is active, avoiding unnecessary computation on suppressed messages and preventing format string injection.- No sensitive data in output
Private keys, passwords, and certificate contents are never included in log messages,
__repr__output, or error messages. Password comparisons usehmac.compare_digest()to prevent timing side-channels.
See Debugging and Logging for logger configuration and practical debugging recipes.
Memory Safety¶
- Immutable protocol objects
Service request and response types, BVLC-SC messages, and other protocol structures are frozen dataclasses (
@dataclass(frozen=True, slots=True)). Once created, their fields cannot be mutated, eliminating a class of state corruption bugs.- Bounded buffers
Trend log and audit log objects use circular buffer management with configurable maximum sizes. When a buffer reaches capacity, the oldest entries are overwritten. This prevents unbounded memory growth from long-running data collection.
- Transport resource caps
All transport layers enforce size limits on network-facing data structures to prevent memory exhaustion from malicious or misbehaving peers:
BBMD – Foreign device tables are capped by
max_fdt_entries(default 128) and broadcast distribution tables bymax_bdt_entries(default 128). Requests exceeding either limit are NAKed. Foreign device registration TTLs are capped at 3600 seconds (1 hour).BACnet/IPv6 – The VMAC resolution cache enforces a
max_entrieslimit (default 4096) with automatic stale-entry eviction. Pending address resolutions are capped at 1024 concurrent VMACs.BACnet/SC – The node switch caps pending address resolutions to
max_connections. The hub function caps active connections viamax_connections.
- Enum vendor caches
The
ObjectType._missing_()andPropertyIdentifier._missing_()vendor-proprietary enum caches are both capped at 4,096 entries. When the cap is reached the cache is cleared, preventing unbounded memory growth from protocol traffic containing many distinct vendor-proprietary type codes.- Change callback cap
ObjectDatabase.register_change_callback()limits each property to 100 registered callbacks, raisingValueErrorif the limit is exceeded. This prevents accidental unbounded growth from repeated registrations.- Constant-time secret comparison
Password verification in server handlers uses
hmac.compare_digest()rather than==, preventing timing attacks that could reveal password length or content through response time variations.
Dependency Posture¶
bac-py has zero required runtime dependencies for the core library. This minimizes the attack surface from third-party code:
Core library – no external packages required; uses only the Python standard library (
asyncio,ssl,struct,logging, etc.)BACnet/SC – optional
websocketsandcryptographypackages, installed viapip install bac-py[secure]JSON serialization – optional
orjsonfor performance, installed viapip install bac-py[serialization]
All optional dependencies are well-maintained, widely-used packages with active security response processes.
Production Checklist¶
When deploying bac-py in production environments:
Enable TLS – never set
allow_plaintext=Trueoutside of development. BACnet/SC requires TLS 1.3 with mutual authentication for production use.Set buffer limits – configure
buffer_sizeon trend log and audit log objects to match available memory. The default circular buffer prevents unbounded growth, but sizing it appropriately avoids unnecessary memory use.Enable INFO logging –
INFOlevel covers lifecycle events and significant operations without per-packet overhead. ReserveDEBUGfor targeted troubleshooting on specific modules.Keep dependencies updated – regularly update optional dependencies (
websockets,cryptography,orjson) to pick up security patches.Bind to specific interfaces – when creating transports, specify the interface address rather than binding to
0.0.0.0to limit exposure.Use passwords for device control – set passwords on DeviceCommunicationControl and ReinitializeDevice handlers to prevent unauthorized device manipulation.