SNMP4J 3.13.0 Release

SNMP4J 3.13.0 has been released 2026-07-27T22:00:00Z which is an unusually heavy security release.

The fixes cluster into a few classes, several of which are genuinely serious rather than hardening cosmetics.

Authentication bypass and spoofing (the most serious group)

  • (D)TLS client authentication was not actually enforced. DefaultSSLEngineConfiguration called setNeedClientAuth(true) and then setWantClientAuth(true), which silently downgrades the requirement. A client presenting no certificate completed the handshake, the resulting SSLPeerUnverifiedException was swallowed, and the message was dispatched as authPriv with an empty security name. That’s an unauthenticated peer being treated as fully authenticated — the highest-impact item here (at least if empty security name is accepted by VACM, which is not the default).
  • No server identity verification on the code path actually in use (CWE-297). TLSTMExtendedTrustManager skipped dNSName/iPAddress SAN matching for SSLEngine, and the boolean result of isServerCertificateAccepted(..) was ignored. Combined, this makes TLS/DTLS clients trivially MITM-able by any certificate chaining to a trusted CA.
  • MPv3 handed the live SecurityStateReference of an outstanding request to USM based on msgID alone (CWE-362/290). USM used it as an output parameter, overwriting security name and auth/priv keys before authentication. An attacker who observes or guesses an in-flight msgID could cause an unrelated response to be emitted under a different user and different keys — cryptographic state corruption driven by an attacker-chosen identifier.
  • Response matching by request ID only (CWE-346/940). No peer, version, or community binding, so any host able to reach the session’s UDP port could answer on behalf of a polled device. This is the classic SNMP response-injection weakness and affects all versions. Now, peer address, version, and community are directly checked by SNMP4J. Peer address check can be disabled for multi-homed, NAT, and other setups, where response address might change.
  • Engine ID cache poisoning via unauthenticated REPORT/RESPONSE PDUs, and msgID-based consumption of outstanding request state (a mismatching response destroyed the legitimate request’s state).

Denial of service

  • Single-packet, persistent DoS against authenticated SNMPv3 (CWE-345): UsmTimeTable latched engineBoots/engineTime from an unauthenticated message. One spoofed datagram with engineBoots = 2147483647 makes every subsequent authenticated message from that engine fail the time-window check until restart. Minimal cost, maximal duration — arguably the best-quality DoS in the set.
  • Infinite CPU spin on one truncated DTLS record (CWE-835): unwrap(..) returned BUFFER_UNDERFLOW without consuming input inside a loop that never re-checked the handshake timeout.
  • Unbounded resource growth (CWE-770/772) in several places: the UsmTimeTable grew without limit from unauthenticated traffic; AbstractConnectionOrientedTransportMapping shadowed connectionTimeout with a zero-initialized field, so the idle socket cleaner was never created for TCP and TLS; the TCP framing-error path leaked SocketEntry objects; DTLS handshake threads and queues were unbounded; and CRL retrieval had no timeouts, with reverse DNS lookups blocking the listen thread.

Input validation / memory safety

  • Unvalidated msgAuthenticationParameters length (CWE-130) caused an out-of-bounds read in AuthGeneric.isAuthentic(..).
  • Unvalidated msgAuthoritativeEngineID length, despite it being a key into the USM user and time tables.

Detection evasion — worth calling out separately

A recurring secondary defect is that failures were swallowed: the OOB-read exception meant usmStatsWrongDigests was never incremented and no AuthenticationFailureEvent fired, so authentication probing was invisible; the TLS unverified-peer path was merely logged. Even where the primary bug was low-impact, the silence made exploitation attempts undetectable.

The common root cause

Almost every item is the same mistake in different clothing: acting on attacker-controlled data before verifying it — trusting msgID, request ID, engine ID, or a half-completed handshake as if it were authenticated. The fixes consistently move the trust decision earlier and make the unauthenticated path bounded and non-mutating.

One operational note: several fixes change behavior by default (mandatory TLS client auth, endpoint identification, engine-time no longer trusted, connection timeouts now active on TCP/TLS). Each has an opt-out flag, but a deployment relying on anonymous DTLS clients or on hostname-mismatched certificates will break on upgrade.

CHANGES

  • SECURITY: The MPv3’s cache for contextEngineIDs could have been poisoned remotely by sending unauthenticated REPORT or RESPONSE PDUs. This issue has been fixed in 3.13.0 by accepting engine ID overwrites in this cache only if the authoritative engine ID has been received via authenticated REPORT or RESPONSE PDUs that matched any outstanding request. This issue did not affect snmp entities not using noAuthNoPriv USM users.
  • SECURITY: UsmTimeTable latched the engineBoots and engineTime of an unauthenticated SNMPv3 message as authoritative for a not yet synchronized engine ID (CWE-345). A single spoofed datagram with msgAuthoritativeEngineBoots = 2147483647 could therefore make all authenticated messages of that engine fail the time window check until the entry was removed explicitly or the process was restarted. Discovered engine IDs are now (again) recorded with engineBoots = 0 and engineTime = 0 as prescribed by RFC 3414 §2.3. This regression was introduced with version 2.4.0, unfortunately. Set SNMP4JSettings.setTrustUnauthenticatedEngineTime(true) to restore the pre 3.13.0 and past 2.4.0 behavior.
  • SECURITY: The UsmTimeTable was unbounded (CWE-770). UsmTimeTable.checkEngineID(..) is the only writer of that table which is reached before a received message has been authenticated, so any peer could grow it without bound by sending unauthenticated messages with an ever changing authoritative engine ID. The number of entries created by engine discovery is now limited to UsmTimeTable.getMaxDiscoveredEntries(), which defaults to SNMP4JSettings.getMaxEngineIdCacheSize() (50000, the same limit the MPv3 engine ID cache uses); set it to 0 for the unbounded pre 3.13.0 behavior. When the limit is reached, the oldest entry that has not been synchronized by an authenticated message yet is evicted. Entries added with UsmTimeTable.addEntry(..) - for example for a target this entity sends requests to - and entries of engines that sent an authenticated message are neither counted nor evicted, so unauthenticated traffic cannot displace the time synchronization of the engines this entity actually talks to. UsmTimeEntry therefore has a new isAuthenticated() flag which UsmTimeTable.checkTime(..) sets: that method must only be called for messages whose authentication digest has been verified, as its (now documented) contract requires.
  • SECURITY: The length of a received msgAuthoritativeEngineID was not validated, although the engine ID is used as a key of the USM user and time tables. An engine ID longer than SNMP4JSettings.getMaxInboundEngineIdLength() - 32 by default, the upper bound of SnmpEngineID as defined by RFC 3411; 0 disables the check - is now rejected while decoding the USM security parameters. Note that the lower bound of SnmpEngineID (5 bytes) is intentionally not enforced on receive, because a zero length engine ID is required for engine discovery and other implementations, net-snmp among them, do not check the size of a received engine ID at all.
  • SECURITY: (D)TLS client authentication was effectively optional, violating RFC 6353 §5.3.2. DefaultSSLEngineConfiguration.configure(..) called setNeedClientAuth(true) immediately followed by setWantClientAuth(true), and the latter overrides the former per the SSLEngine contract. A client that sent no certificate could therefore complete the handshake, the resulting SSLPeerUnverifiedException was logged and swallowed, and the message was delivered as an authPriv message with an empty security name. Client authentication is now required, the unverified peer path invalidates the session, increments snmpTlstmSessionInvalidClientCertificates and snmpTlstmSessionOpenErrors, and closes the connection instead of dispatching the message. Use DefaultSSLEngineConfiguration.setClientAuthenticationRequired(false) if anonymous (D)TLS clients have to be accepted. In addition, TSM.processIncomingMsg(..) now rejects a message of an accepted transport connection whose tmSecurityName is empty with SNMPv3_TSM_INVALID_CACHES, which also fixes a NullPointerException with TSM(usePrefix = true).
  • SECURITY: One truncated DTLS record made a DTLS handshake worker spin at 100% CPU indefinitely (CWE-835), because SSLEngine.unwrap(..) returned BUFFER_UNDERFLOW without consuming a byte and the same buffer was unwrapped again in an inner loop that never re-evaluated the handshake timeout. A datagram whose unwrap make no progress is now discarded and the outer loop re-evaluates the handshake timeout and the loop budget.
  • SECURITY: The DTLSTM listen thread could be parked indefinitely by unauthenticated datagrams (CWE-770). DTLSTM.prepareInPacket(..) now uses ThreadPool.tryToExecute(..) and drops the datagram when all handshake threads are busy (DTLS retransmits), the SSLEngine of an accepted session is created with the peer IP address instead of its reverse resolved host name (which could block the listen thread in a PTR lookup), the per peer handshake packet queue is bounded (see DTLSTM.setMaxInboundHandshakeQueueSize(int), default 16), and unchecked exceptions of the SocketEntry creation no longer escape the listen thread. TLSTMUtil now applies connect and read timeouts when a certificate revocation list is retrieved (see TLSTMUtil.setCrlFetchTimeoutMillis(int), default 10000 ms) and reports failures as GeneralSecurityException instead of an unchecked RuntimeException.
    Key store and trust store contents are cached by file modification time and size, so a new connection no longer reads and parses both stores from disk again (see TLSTMUtil.clearKeyStoreCache()).
    ThreadPool.TaskManager.run() now clears the current task in a finally block, so a task that throws a Throwable no longer leaves its worker permanently non-idle.
  • SECURITY: TLSTMExtendedTrustManager did not verify the identity of the (D)TLS server on the SSLEngine code path that TLSTM and DTLSTM actually use (CWE-297, RFC 6353 §5.3.1). The expected identity of a CertifiedIdentity target - see CertifiedIdentity.getConfiguredIdentity() and CertifiedTarget.setIdentity(..) below - is now matched against the dnsName (with left-most label wildcard) and ipAddress subject alternative names of the server certificate, client mode SSL engines use the “HTTPS” endpoint identification algorithm by default (see DefaultSSLEngineConfiguration.setEndpointIdentificationAlgorithm(String), set it to null to disable; it is not applied to a target with a pinned server fingerprint, because RFC 6353 lets the pin replace the identity comparison - this uses the new SSLEngineConfigurator.configure(SSLEngine, TransportStateReference) which defaults to the existing configure(SSLEngine)), the boolean result of TlsTmSecurityCallback.isServerCertificateAccepted(..) is enforced (see SNMP4JSettings.setEnforceTlsSecurityCallbackResult(boolean)), and the security callback configured on a TlsX509CertifiedTarget is now used in preference to the transport mapping wide callback.
  • SECURITY: MPv3 handed the live SecurityStateReference of an outstanding request to the security model, based on the msgID of the received message alone (CWE-362, CWE-290). USM used that object as an output parameter and overwrote its security name and its authentication and privacy keys before the message was authenticated, so a peer that observed or guessed an in-flight msgID could make the response of an unrelated request be emitted with a different msgUserName and different keys. A cached security state reference is now only reused for an outgoing request of this entity whose security model and address match the received message, USM no longer writes the security name of a state reference that is cached for response processing, and the security name is written only after the user has been resolved successfully.
  • SECURITY: An attacker supplied msgID could consume or displace the state of an outstanding SNMPv3 request (CWE-346). MPv3.validatePreparedPDU(..) removed the cache entry before performing the RFC 3412 §7.2.11/§7.2.12 match checks, so a mismatching response or report destroyed the state of the legitimate request. The entry is now validated first and consumed atomically only when it matches (see MPv3.Cache.consumeEntry(..)), and MPv3.Cache.addEntry(..) no longer replaces the msgID mapping of another live cache entry.
    Set SNMP4JSettings.setVerifyResponsePeerAddress(true) to additionally require that a response or report is received from the address the request has been sent to.
  • SECURITY: Responses were matched to outstanding requests by their request ID alone, with no peer, version, or community binding (CWE-346, CWE-940), so any host able to send a UDP packet to the port of a session could answer on behalf of a polled device. Snmp.processPdu(..) now verifies the message processing model and, for CommunityTarget, the community of the response before the response is accepted. A mismatching response leaves the pending request in place so that the genuine response can still be delivered, and increments the new counter snmp4jStatsRequestUnmatchedResponses (1.3.6.1.4.1.4976.10.1.1.4.1.1.5.0). Peer address verification is available with SNMP4JSettings.setVerifyResponsePeerAddress(true) but disabled by default, because devices legitimately respond from another address than the one that has been polled (source-interface or management VRF configurations, NAT, dual stack).
  • SECURITY: AbstractConnectionOrientedTransportMapping shadowed the connectionTimeout field of AbstractTransportMapping with a field initialized to zero (CWE-772, CWE-770). The idle socket cleaner was therefore never created for TCP and TLS, so a peer could keep an unbounded number of accepted connections and their buffers alive forever. The inherited default of 60000 ms now applies to server enabled transport mappings, the socket cleaner is created unconditionally in listen() so that a later setConnectionTimeout(..) takes effect, TLSTM.SocketEntry stores its scheduled socket timeout so that it can be canceled, TLSTM.SocketEntry allocates its three buffers on first use instead of in the accept path, and Snmp.addNotificationListener(..) no longer forces the connection timeout of a connection-oriented transport mapping to zero. The new AbstractConnectionOrientedTransportMapping.setMaxInboundConnections(int) limits the number of concurrently accepted connections (0, no limit, is the default).
  • SECURITY: USM did not validate the length of msgAuthenticationParameters (CWE-130). An undersized field made AuthGeneric.isAuthentic(..) read past the end of the received message and throw an ArrayIndexOutOfBoundsException that was swallowed without incrementing usmStatsWrongDigests or firing an AuthenticationFailureEvent, so this vainly authentication probing was invisible in the statistics. The length is now checked against the authentication code length of the user’s authentication protocol as required by RFC 3414 §3.2.6, and the ByteArrayWindow constructor rejects a window that exceeds its underlying array.
  • SECURITY: The framing error path of DefaultTcpTransportMapping closed the socket but neither removed the SocketEntry from the sockets map nor fired a TransportStateEvent (CWE-772), so a peer could accumulate stale entries with a six-byte message and TransportStateListeners never learned that those connections died. The branch now throws an IOException, which makes the server thread perform its complete teardown.
  • Fixed: SnmpTLSFingerprint values (RFC 6353) are now matched with the hash algorithm that is identified by the one octet hash algorithm identifier of the IANA TLS HashAlgorithm registry which prefixes such a value.
    Before, the hash algorithm was derived from the signature algorithm of the certificate and the identifier octet was compared as if it were part of the hash value, so a fingerprint that was configured as defined by RFC 6353 (for example through the SNMP-TLS-TM-MIB) never matched. Fingerprints without that identifier octet are still accepted to stay compatible with configurations created for SNMP4J before 3.13.0.
    New API: TLSTMUtil.isMatchingFingerprint(X509Certificate,OctetString), TLSTMUtil.getFingerprint(X509Certificate,int), TLSTMUtil.getHashAlgorithmName(int), and
    TLSTMUtil.digest(X509Certificate,String).
  • Fixed: TLSTMUtil.getIpAddressFromSubjAltName(..) now returns the 32 character all lowercase hexadecimal string without separators that RFC 6353 defines for mapping an IPv6 ipAddress subjectAltName to a tmSecurityName. Before, each colon separated group was zero padded to two instead of four characters and neither the ‘::’ zero compression nor an embedded IPv4 address was expanded. Invalid addresses are now ignored (i.e. null is returned) instead of being mapped to a malformed name.
    New API: TLSTMUtil.formatIpAddressForTmSecurityName(String).
  • Added: CertifiedTarget.setIdentity(OctetString) to set the identity (hostname or IP address) of a CertifiedTarget independently from its securityName. Before version 3.13.0, the identity was always identical to the securityName, although the two answer different questions and RFC 6353 keeps them in different tables: the securityName is the tmSecurityName that TSM copies into msgSecurityName for access control (snmpTargetParamsSecurityName), whereas the identity is what the peer has to prove with its certificate (snmpTlstmAddrServerIdentity). CertifiedTarget.getIdentity() still returns the securityName if no identity has been set, so nothing changes for existing code. The RFC 6353 §5.3.1 verification of the TLSTMExtendedTrustManager is based on the new CertifiedIdentity.getConfiguredIdentity() instead, which does not fall back to the securityName: the peer’s dnsName or ipAddress is therefore only checked against the identity of a target whose identity has been set deliberately. A securityName that is not a hostname or IP address thus cannot fail that check, and a target may use an arbitrary securityName for access control while still requesting the identity verification. TargetBuilder got a matching serverIdentity(..) step.
  • Improved: Snmp.ReportProcessor.checkReport(..) now checks for exactly one VariableBinding with a Counter32 value (RFC3412 §6.4). Before this, change, additional VBs or a non Counter32 value would have been ignored.
  • Improved: By default, BER.decodeOID(BERInputStream is, MutableByte type) now checks for truncated OIDs and subid overflows. Both checks can be disabled by setting the BERInputStream.checkOIDOverflows and BERInputStream.checkOIDTruncation properties to false activating pre 3.13.0 behavior.
  • Changed: Added Serializable interface to Address, Variable, SecurityModel, TransportMapping, and UsmUserEntry. Made non-serializable fields explicitly transient in several Event objects.
  • Deferred: The TCP read buffer is still allocated with the declared message length instead of growing as bytes arrive, and complete SSLContext instances are still created per connection. Both are bounded now by the restored idle socket cleaner and by setMaxInboundConnections(int).
1 Like