on master help sign in
Fix federation ack race, presence capacity, auth case, gossip spoofing, and URL injection

- Remove knownPeer presence check from ForwardMessage and FederationAck to fix race where valid acks/forwards are rejected when peer presence expires between message send and ack arrival (federation secret is sufficient auth) - Evict oldest presence entry when table is at capacity in VerifyAuth instead of silently dropping the new user's presence - Accept case-insensitive "Bearer"/"bearer" prefix in extractToken per RFC 7235 - Require e.server == fromServer unconditionally in RecvGossip to prevent authenticated peers from injecting fake presence for arbitrary servers via low hopsRemaining values - Add containsBadUrlChar validation on server hostnames in gossip events, ForwardMessage originServer, and FederationAck fromServer to prevent URL injection in federation HTTP calls

commit1496a579eafd6c1484fab486f524eee87f79af4a authoriko <mail@iko.soy> dateSun 29 Mar 2026, 14:23 (7 months ago) parent715d1e2
1 file changed+28−25
server.knot+28−25
server.knot+28−25
@@ -101,6 +101,12 @@ tooLong = \s maxLen -> not (drop maxLen s == "")
101101
102102isValidHex = \s -> not (s == "") && bytesToHex (bytesFromHex s) == s
103103
104+containsBadUrlChar = \s ->
105+ if s == "" then False {}
106+ else if take 1 s == "/" || take 1 s == "?" || take 1 s == "#" || take 1 s == " " || take 1 s == "@"
107+ then True {}
108+ else containsBadUrlChar (drop 1 s)
109+
104110lookupSession = \sessions token t ->
105111 single (do
106112 s <- sessions
@@ -109,7 +115,7 @@ lookupSession = \sessions token t ->
109115 yield s)
110116
111117extractToken = \authHeader ->
112− if take 7 authHeader == "Bearer "
118+ if take 7 authHeader == "Bearer " || take 7 authHeader == "bearer "
113119 then drop 7 authHeader
114120 else ""
115121
@@ -279,10 +285,15 @@ serve = \req -> case req of
279285 p <- presence
280286 where not (p.pubkey == pubkey && p.server == serverHost)
281287 yield p
288+ let oldestPresenceExp = fold (\acc p -> if acc == -1 || p.expiresAt < acc then p.expiresAt else acc) -1 presenceWithout
289+ let presenceEvicted = if count presenceWithout >= maxPresenceEntries && oldestPresenceExp >= 0
290+ then (fold (\acc p ->
291+ if not acc.removed && p.expiresAt == oldestPresenceExp
292+ then {removed: True {}, items: acc.items}
293+ else {removed: acc.removed, items: union acc.items [p]}) {removed: False {}, items: []} presenceWithout).items
294+ else presenceWithout
282295 let updatedPresence = if sigValid
283− then if count presenceWithout < maxPresenceEntries
284− then union presenceWithout [{pubkey: pubkey, server: serverHost, expiresAt: expiry, timestamp: t}]
285− else presenceWithout
296+ then union presenceEvicted [{pubkey: pubkey, server: serverHost, expiresAt: expiry, timestamp: t}]
286297 else presence
287298 set *presence = updatedPresence
288299 let peerServers = if sigValid && not (federationSecret == "")
@@ -492,7 +503,7 @@ serve = \req -> case req of
492503 t <- now
493504 let authorized = verifyFederationAuth authorization
494505 let cappedHops = if hopsRemaining > 5 then 5 else hopsRemaining
495− let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && count events <= maxGossipEvents
506+ let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && not (containsBadUrlChar fromServer) && count events <= maxGossipEvents
496507 let onlineEntries = do
497508 e <- events
498509 where validRequest
@@ -501,8 +512,9 @@ serve = \req -> case req of
501512 where not (e.pubkey == "")
502513 where not (tooLong e.pubkey maxPubkeyLen)
503514 where not (tooLong e.server maxPubkeyLen)
515+ where not (containsBadUrlChar e.server)
504516 where isValidHex e.pubkey
505− where cappedHops < 2 || e.server == fromServer
517+ where e.server == fromServer
506518 where not (e.server == serverHost)
507519 where e.timestamp <= t + clockSkewTolerance
508520 where e.timestamp + (clampTtl e.ttl) * 1000 > t
@@ -515,8 +527,9 @@ serve = \req -> case req of
515527 where not (e.pubkey == "")
516528 where not (tooLong e.pubkey maxPubkeyLen)
517529 where not (tooLong e.server maxPubkeyLen)
530+ where not (containsBadUrlChar e.server)
518531 where isValidHex e.pubkey
519− where cappedHops < 2 || e.server == fromServer
532+ where e.server == fromServer
520533 where not (e.server == serverHost)
521534 where e.timestamp <= t + clockSkewTolerance
522535 where t - e.timestamp < (clampTtl e.ttl) * 1000
@@ -666,8 +679,9 @@ serve = \req -> case req of
666679 where not (e.pubkey == "")
667680 where not (tooLong e.pubkey maxPubkeyLen)
668681 where not (tooLong e.server maxPubkeyLen)
682+ where not (containsBadUrlChar e.server)
669683 where isValidHex e.pubkey
670− where cappedHops < 2 || e.server == fromServer
684+ where e.server == fromServer
671685 where not (e.server == serverHost)
672686 where e.timestamp <= t + clockSkewTolerance
673687 where e.timestamp + (clampTtl e.ttl) * 1000 > t
@@ -702,19 +716,13 @@ serve = \req -> case req of
702716 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
703717 t <- now
704718 let authorized = verifyFederationAuth authorization
705− let validOrigin = not (originServer == "") && not (originServer == serverHost) && not (tooLong originServer maxPubkeyLen)
719+ let validOrigin = not (originServer == "") && not (originServer == serverHost) && not (tooLong originServer maxPubkeyLen) && not (containsBadUrlChar originServer)
706720 let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "") && not (tooLong toKey maxPubkeyLen) && not (tooLong fromKey maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen) && not (tooLong messageId maxPubkeyLen) && isValidHex toKey && isValidHex fromKey && isValidHex encryptedBlob && isValidHex messageId
707721 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
708722 result <- atomic do
709723 pr <- pruneExpired t
710− presence <- *presence
711724 messages <- *messages
712725 seqCounter <- *seqCounter
713− let knownPeer = count (do
714− p <- presence
715− where p.server == originServer
716− where p.expiresAt > t
717− yield p) > 0
718726 let seqTs = if t > seqCounter then t else seqCounter + 1
719727 let exists = count (do
720728 m <- messages
@@ -722,15 +730,15 @@ serve = \req -> case req of
722730 where m.toKey == toKey
723731 where m.receivedAt >= t - messageMaxAge
724732 yield m) > 0
725− let shouldInsert = authorized && validOrigin && validFields && knownPeer && not exists
733+ let shouldInsert = authorized && validOrigin && validFields && not exists
726734 set *seqCounter = if shouldInsert then seqTs else seqCounter
727735 set *messages = if shouldInsert
728736 then union messages
729737 [{messageId: messageId, toKey: toKey, fromKey: fromKey,
730738 encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: originServer, receivedAt: seqTs}]
731739 else messages
732− yield {peerServers: pr.peerServers, expiredLocal: pr.expiredLocal, knownPeer: knownPeer}
733− respond {ok: authorized && validOrigin && validFields && result.knownPeer}
740+ yield {peerServers: pr.peerServers, expiredLocal: pr.expiredLocal}
741+ respond {ok: authorized && validOrigin && validFields}
734742 broadcastOfflineGossip t result.peerServers result.expiredLocal
735743
736744 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
@@ -743,18 +751,13 @@ serve = \req -> case req of
743751 else if not (isValidHex mid) then acc
744752 else if count (do a <- acc; where a == mid; yield a) > 0 then acc
745753 else union acc [mid]) [] messageIds
746− let validAckFields = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen)
754+ let validAckFields = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost) && not (tooLong fromServer maxPubkeyLen) && not (containsBadUrlChar fromServer)
747755 pruneResult <- atomic do
748756 pr <- pruneExpired t
749757 messages <- *messages
750758 presence <- *presence
751759 forwards <- *forwards
752− let knownPeer = count (do
753− p <- presence
754− where p.server == fromServer
755− where p.expiresAt > t
756− yield p) > 0
757− let validAck = validAckFields && knownPeer
760+ let validAck = validAckFields
758761 set *messages = if validAck
759762 then diff messages (do
760763 m <- messages