From 985b983f67dcf8fc8e1880afbe48b4f3b8dfb497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Fri, 17 Jul 2026 17:32:01 +0200 Subject: [PATCH] TrafficManagement: gate role/NodeInfo cache writes on signer authenticity (#11048) * TrafficManagement: gate role/NodeInfo cache writes on signer authenticity The tier-3 role cache and the PSRAM NodeInfo response cache were updated from any received NodeInfo with no authenticity check, so a spoofed NodeInfo could set a node's cached role (granting dedup exceptions) or poison the cached user served in direct responses. Skip both cache writes when a known signer's NodeInfo arrives unsigned, matching the identity-update gate on the direct-response path. * TrafficManagement: hoist shared NodeInfo signer lookup Compute the sender node lookup and unauthenticated-signer check once per NodeInfo packet and reuse it for both the cache-refresh gate and the direct-response identity gate, avoiding a second O(N) getMeshNode scan. --------- Co-authored-by: Ben Meadors --- src/modules/TrafficManagementModule.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index f10a9f0d4..74e8de6c2 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -726,9 +726,16 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack return ProcessMessage::CONTINUE; } + // A known signer's NodeInfo arriving unsigned is unauthenticated (the sender is forgeable), so + // it must not drive any cache or identity write below. Computed once here (getMeshNode is O(N)) + // and reused by both the cache-refresh and the direct-response identity path. + const bool isNodeInfo = mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP; + const meshtastic_NodeInfoLite *senderNode = isNodeInfo ? nodeDB->getMeshNode(getFrom(&mp)) : nullptr; + const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed; + // Learn NodeInfo payloads into the dedicated PSRAM cache, and refresh the tier-3 // role cache for any node we already track (keeps the dedup role exception current). - if (mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP) { + if (isNodeInfo && !unauthenticatedSigner) { cacheNodeInfoPacket(mp); updateCachedRoleFromNodeInfo(mp); } @@ -746,8 +753,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack if (shouldRespondToNodeInfo(&mp, true)) { // Unicast NodeInfo is never signed, so a known signer's identity claim here is // unauthenticated: don't overwrite its stored name. The cached response is unaffected. - const meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp)); - const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed; + // unauthenticatedSigner was computed above (this branch is NodeInfo-only). meshtastic_User requester = meshtastic_User_init_zero; if (!unauthenticatedSigner && pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) {