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.
DefaultSSLEngineConfigurationcalledsetNeedClientAuth(true)and thensetWantClientAuth(true), which silently downgrades the requirement. A client presenting no certificate completed the handshake, the resultingSSLPeerUnverifiedExceptionwas 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).
TLSTMExtendedTrustManagerskipped dNSName/iPAddress SAN matching forSSLEngine, and the boolean result ofisServerCertificateAccepted(..)was ignored. Combined, this makes TLS/DTLS clients trivially MITM-able by any certificate chaining to a trusted CA. - MPv3 handed the live
SecurityStateReferenceof 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):
UsmTimeTablelatchedengineBoots/engineTimefrom an unauthenticated message. One spoofed datagram withengineBoots = 2147483647makes 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(..)returnedBUFFER_UNDERFLOWwithout consuming input inside a loop that never re-checked the handshake timeout. - Unbounded resource growth (CWE-770/772) in several places: the
UsmTimeTablegrew without limit from unauthenticated traffic;AbstractConnectionOrientedTransportMappingshadowedconnectionTimeoutwith a zero-initialized field, so the idle socket cleaner was never created for TCP and TLS; the TCP framing-error path leakedSocketEntryobjects; 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
msgAuthenticationParameterslength (CWE-130) caused an out-of-bounds read inAuthGeneric.isAuthentic(..). - Unvalidated
msgAuthoritativeEngineIDlength, 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 forcontextEngineIDscould 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 usingnoAuthNoPrivUSMusers. - SECURITY:
UsmTimeTablelatched theengineBootsandengineTimeof an unauthenticated SNMPv3 message as authoritative for a not yet synchronized engine ID (CWE-345). A single spoofed datagram withmsgAuthoritativeEngineBoots= 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 withengineBoots= 0 andengineTime= 0 as prescribed by RFC 3414 §2.3. This regression was introduced with version 2.4.0, unfortunately. SetSNMP4JSettings.setTrustUnauthenticatedEngineTime(true)to restore the pre 3.13.0 and past 2.4.0 behavior. - SECURITY: The
UsmTimeTablewas 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 toUsmTimeTable.getMaxDiscoveredEntries(), which defaults toSNMP4JSettings.getMaxEngineIdCacheSize()(50000, the same limit theMPv3engine 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 withUsmTimeTable.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.UsmTimeEntrytherefore has a new isAuthenticated() flag whichUsmTimeTable.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
msgAuthoritativeEngineIDwas not validated, although the engine ID is used as a key of theUSMuser and time tables. An engine ID longer thanSNMP4JSettings.getMaxInboundEngineIdLength() - 32by default, the upper bound ofSnmpEngineIDas defined by RFC 3411; 0 disables the check - is now rejected while decoding the USM security parameters. Note that the lower bound ofSnmpEngineID(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(..)calledsetNeedClientAuth(true)immediately followed bysetWantClientAuth(true), and the latter overrides the former per theSSLEnginecontract. A client that sent no certificate could therefore complete the handshake, the resultingSSLPeerUnverifiedExceptionwas logged and swallowed, and the message was delivered as anauthPrivmessage with an empty security name. Client authentication is now required, the unverified peer path invalidates the session, incrementssnmpTlstmSessionInvalidClientCertificatesandsnmpTlstmSessionOpenErrors, and closes the connection instead of dispatching the message. UseDefaultSSLEngineConfiguration.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 withSNMPv3_TSM_INVALID_CACHES, which also fixes aNullPointerExceptionwithTSM(usePrefix = true). - SECURITY: One truncated DTLS record made a DTLS handshake worker spin at 100% CPU indefinitely (CWE-835), because
SSLEngine.unwrap(..) returnedBUFFER_UNDERFLOWwithout 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
DTLSTMlisten thread could be parked indefinitely by unauthenticated datagrams (CWE-770).DTLSTM.prepareInPacket(..)now usesThreadPool.tryToExecute(..)and drops the datagram when all handshake threads are busy (DTLS retransmits), theSSLEngineof 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 (seeDTLSTM.setMaxInboundHandshakeQueueSize(int), default 16), and unchecked exceptions of the SocketEntry creation no longer escape the listen thread.TLSTMUtilnow applies connect and read timeouts when a certificate revocation list is retrieved (seeTLSTMUtil.setCrlFetchTimeoutMillis(int), default 10000 ms) and reports failures asGeneralSecurityExceptioninstead of an uncheckedRuntimeException.
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 (seeTLSTMUtil.clearKeyStoreCache()).
ThreadPool.TaskManager.run()now clears the current task in a finally block, so a task that throws aThrowableno longer leaves its worker permanently non-idle. - SECURITY:
TLSTMExtendedTrustManagerdid not verify the identity of the (D)TLS server on theSSLEnginecode path thatTLSTMandDTLSTMactually use (CWE-297, RFC 6353 §5.3.1). The expected identity of aCertifiedIdentitytarget - seeCertifiedIdentity.getConfiguredIdentity()andCertifiedTarget.setIdentity(..) below - is now matched against thednsName(with left-most label wildcard) andipAddresssubject alternative names of the server certificate, client mode SSL engines use the “HTTPS” endpoint identification algorithm by default (seeDefaultSSLEngineConfiguration.setEndpointIdentificationAlgorithm(String), set it tonullto 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 newSSLEngineConfigurator.configure(SSLEngine, TransportStateReference)which defaults to the existingconfigure(SSLEngine)), the boolean result ofTlsTmSecurityCallback.isServerCertificateAccepted(..)is enforced (seeSNMP4JSettings.setEnforceTlsSecurityCallbackResult(boolean)), and the security callback configured on aTlsX509CertifiedTargetis now used in preference to the transport mapping wide callback. - SECURITY:
MPv3handed the liveSecurityStateReferenceof 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 differentmsgUserNameand 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
msgIDcould 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 (seeMPv3.Cache.consumeEntry(..)), andMPv3.Cache.addEntry(..)no longer replaces themsgIDmapping of another live cache entry.
SetSNMP4JSettings.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 withSNMP4JSettings.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
AbstractTransportMappingwith 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 latersetConnectionTimeout(..)takes effect,TLSTM.SocketEntrystores its scheduled socket timeout so that it can be canceled,TLSTM.SocketEntryallocates its three buffers on first use instead of in the accept path, andSnmp.addNotificationListener(..)no longer forces the connection timeout of a connection-oriented transport mapping to zero. The newAbstractConnectionOrientedTransportMapping.setMaxInboundConnections(int)limits the number of concurrently accepted connections (0, no limit, is the default). - SECURITY:
USMdid not validate the length ofmsgAuthenticationParameters(CWE-130). An undersized field madeAuthGeneric.isAuthentic(..)read past the end of the received message and throw anArrayIndexOutOfBoundsExceptionthat was swallowed without incrementingusmStatsWrongDigestsor firing anAuthenticationFailureEvent, 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 theByteArrayWindowconstructor rejects a window that exceeds its underlying array. - SECURITY: The framing error path of DefaultTcpTransportMapping closed the socket but neither removed the
SocketEntryfrom the sockets map nor fired aTransportStateEvent(CWE-772), so a peer could accumulate stale entries with a six-byte message andTransportStateListenersnever learned that those connections died. The branch now throws anIOException, which makes the server thread perform its complete teardown. - Fixed:
SnmpTLSFingerprintvalues (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 ipAddresssubjectAltNameto atmSecurityName. 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.nullis 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
CertifiedTargetindependently from its securityName. Before version 3.13.0, the identity was always identical to thesecurityName, although the two answer different questions and RFC 6353 keeps them in different tables: thesecurityNameis thetmSecurityNamethatTSMcopies intomsgSecurityNamefor access control (snmpTargetParamsSecurityName), whereas the identity is what the peer has to prove with its certificate (snmpTlstmAddrServerIdentity).CertifiedTarget.getIdentity()still returns thesecurityNameif no identity has been set, so nothing changes for existing code. The RFC 6353 §5.3.1 verification of theTLSTMExtendedTrustManageris based on the newCertifiedIdentity.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. AsecurityNamethat is not a hostname or IP address thus cannot fail that check, and a target may use an arbitrarysecurityNamefor access control while still requesting the identity verification.TargetBuildergot a matching serverIdentity(..) step. - Improved:
Snmp.ReportProcessor.checkReport(..)now checks for exactly oneVariableBindingwith aCounter32value (RFC3412 §6.4). Before this, change, additional VBs or a nonCounter32value 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 theBERInputStream.checkOIDOverflowsandBERInputStream.checkOIDTruncationproperties to false activating pre 3.13.0 behavior. - Changed: Added Serializable interface to
Address,Variable,SecurityModel,TransportMapping, andUsmUserEntry. 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).