on master help sign in
Fix 6 server.knot bugs: offline timestamp estimation, fromServer validation, LookupPresence unauthorized response, AckMessages empty IDs, Poll duplicate gossip, and input size validation

- Add timestamp field to presence type for precise offline event checks - Store creation timestamp with presence entries instead of estimating from expiresAt - Add fromServer validation to RecvGossip to prevent gossip bounce-back - Add authorized field to LookupPresence response to distinguish unauthorized from not-found - Filter empty messageIds in AckMessages and cap list at maxMessageIds - Add post-prune check in Poll to verify no remaining local presence after expiration - Add input size validation constants and checks throughout handlers - Add tooLong helper and validate pubkey, blob, and list sizes at all entry points - Add validRequest guard to RecvGossip event processing to short-circuit on invalid requests

commit5ec38a47d6405b6a4f4d51cc96213ef69e686edb authoriko <mail@iko.soy> dateThu 26 Mar 2026, 14:46 (7 months ago) parent7945fd9
1 file changed+58−45
server.knot+58−45
server.knot+58−45
@@ -11,6 +11,11 @@ federationSecret = "" -- must be set to enable federation
1111federationScheme = "https://" -- scheme for federation HTTP calls
1212messageMaxAge = 86400000 -- 24 hours in milliseconds
1313maxPresenceTtl = 3600 -- max presence TTL from gossip in seconds
14+maxPubkeyLen = 256
15+maxBlobLen = 65536
16+maxSignatureLen = 256
17+maxMessageIds = 100
18+maxGossipEvents = 100
1419
1520-- ============================================
1621-- Types
@@ -30,7 +35,7 @@ type GossipEvent = {eventType: Text, pubkey: Text, server: Text, timestamp: Int,
3035
3136*messages : [{messageId: Text, toKey: Text, fromKey: Text, encryptedBlob: Text, timestamp: Int, originServer: Text, receivedAt: Int}]
3237
33−*presence : [{pubkey: Text, server: Text, expiresAt: Int}]
38+*presence : [{pubkey: Text, server: Text, expiresAt: Int, timestamp: Int}]
3439
3540-- ============================================
3641-- Routes
@@ -57,7 +62,7 @@ route ClientApi where
5762 -> {ok: Bool}
5863 = AckMessages
5964 GET /lookup/{pubkey: Text} headers {authorization: Text}
60− -> {online: Bool, server: Text}
65+ -> {online: Bool, server: Text, authorized: Bool}
6166 = LookupPresence
6267
6368route FederationApi where
@@ -85,6 +90,8 @@ genToken = do
8590 d <- randomInt 2147483647
8691 yield (bytesToHex (textToBytes (show a ++ "." ++ show b ++ "." ++ show c ++ "." ++ show d)))
8792
93+tooLong = \s maxLen -> not (drop maxLen s == "")
94+
8895lookupSession = \sessions token t ->
8996 single (do
9097 s <- sessions
@@ -156,7 +163,7 @@ pruneExpired = \t -> do
156163serve = \req -> case req of
157164
158165 ReqChallenge {pubkey, respond} ->
159− if pubkey == ""
166+ if pubkey == "" || tooLong pubkey maxPubkeyLen
160167 then respond {challenge: ""}
161168 else do
162169 challenge <- genToken
@@ -214,7 +221,7 @@ serve = \req -> case req of
214221 p <- presence
215222 where not (p.pubkey == pubkey && p.server == serverHost)
216223 yield p)
217− [{pubkey: pubkey, server: serverHost, expiresAt: expiry}]
224+ [{pubkey: pubkey, server: serverHost, expiresAt: expiry, timestamp: t}]
218225 else presence
219226 set *presence = updatedPresence
220227 let peerServers = if sigValid && not (federationSecret == "")
@@ -239,12 +246,18 @@ serve = \req -> case req of
239246 let token = extractToken authorization
240247 result <- atomic do
241248 presenceBeforePrune <- *presence
249+ pruneExpired t
250+ presenceAfterPrune <- *presence
242251 let expiredLocal = do
243252 p <- presenceBeforePrune
244253 where p.expiresAt <= t
245254 where p.server == serverHost
255+ where count (do
256+ q <- presenceAfterPrune
257+ where q.pubkey == p.pubkey
258+ where q.server == serverHost
259+ yield q) == 0
246260 yield p
247− pruneExpired t
248261 sessions <- *sessions
249262 messages <- *messages
250263 let session = lookupSession sessions token t
@@ -289,7 +302,7 @@ serve = \req -> case req of
289302 t <- now
290303 msgId <- genToken
291304 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
292− let validInput = not (to == "") && not (encryptedBlob == "")
305+ let validInput = not (to == "") && not (encryptedBlob == "") && not (tooLong to maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen)
293306 result <- atomic do
294307 pruneExpired t
295308 sessions <- *sessions
@@ -344,7 +357,9 @@ serve = \req -> case req of
344357 AckMessages {messageIds, authorization, respond} -> do
345358 t <- now
346359 let dedupedIds = fold (\acc mid ->
347− if count (do a <- acc; where a == mid; yield a) > 0 then acc
360+ if mid == "" then acc
361+ else if count acc >= maxMessageIds then acc
362+ else if count (do a <- acc; where a == mid; yield a) > 0 then acc
348363 else union acc [mid]) [] messageIds
349364 result <- atomic do
350365 pruneExpired t
@@ -383,47 +398,43 @@ serve = \req -> case req of
383398 else yield {}
384399 respond {ok: result.ok}
385400
386− LookupPresence {pubkey, authorization, respond} ->
387− if pubkey == ""
388− then respond {online: False {}, server: ""}
389− else do
390− t <- now
391− result <- atomic do
392− pruneExpired t
393− sessions <- *sessions
394− presence <- *presence
395− let session = lookupSession sessions (extractToken authorization) t
396− let valid = do
397− p <- presence
398− where p.pubkey == pubkey
399− where p.expiresAt > t
400− yield p
401− let foundServer = case session of
402− Nothing {} -> ""
403− Just {value: _} ->
404− if count valid == 0
405− then ""
406− else case single (do
407− p <- valid
408− where p.server == serverHost
409− yield p) of
410− Just {value: p} -> p.server
411− Nothing {} -> fold (\_ p -> p.server) "" valid
412− let authorized = case session of
413− Nothing {} -> False {}
414− _ -> True {}
415− yield {foundServer: foundServer, authorized: authorized}
416− if result.authorized
417− then respond {online: not (result.foundServer == ""), server: result.foundServer}
418− else respond {online: False {}, server: ""}
401+ LookupPresence {pubkey, authorization, respond} -> do
402+ t <- now
403+ result <- atomic do
404+ pruneExpired t
405+ sessions <- *sessions
406+ presence <- *presence
407+ let session = lookupSession sessions (extractToken authorization) t
408+ let authorized = case session of
409+ Nothing {} -> False {}
410+ _ -> True {}
411+ let valid = do
412+ p <- presence
413+ where p.pubkey == pubkey
414+ where p.expiresAt > t
415+ yield p
416+ let foundServer = case session of
417+ Nothing {} -> ""
418+ Just {value: _} ->
419+ if pubkey == "" || tooLong pubkey maxPubkeyLen || count valid == 0
420+ then ""
421+ else case single (do
422+ p <- valid
423+ where p.server == serverHost
424+ yield p) of
425+ Just {value: p} -> p.server
426+ Nothing {} -> fold (\_ p -> p.server) "" valid
427+ yield {foundServer: foundServer, authorized: authorized}
428+ respond {online: not (result.foundServer == ""), server: result.foundServer, authorized: result.authorized}
419429
420430 RecvGossip {events, hopsRemaining, fromServer, authorization, respond} -> do
421431 t <- now
422432 let authorized = verifyFederationAuth authorization
423433 let cappedHops = if hopsRemaining > 5 then 5 else hopsRemaining
424− let validRequest = authorized && cappedHops >= 1
434+ let validRequest = authorized && cappedHops >= 1 && not (fromServer == "") && count events <= maxGossipEvents
425435 let onlineEntries = do
426436 e <- events
437+ where validRequest
427438 where e.eventType == "online"
428439 where not (e.server == "")
429440 where not (e.pubkey == "")
@@ -433,6 +444,7 @@ serve = \req -> case req of
433444 yield {pubkey: e.pubkey, server: e.server, expiresAt: e.timestamp + (clampTtl e.ttl) * 1000, timestamp: e.timestamp}
434445 let offlineEntries = do
435446 e <- events
447+ where validRequest
436448 where e.eventType == "offline"
437449 where not (e.server == "")
438450 where not (e.pubkey == "")
@@ -448,7 +460,7 @@ serve = \req -> case req of
448460 where o.server == e.server
449461 where o.timestamp >= e.timestamp
450462 yield o) == 0
451− yield {pubkey: e.pubkey, server: e.server, expiresAt: e.expiresAt}
463+ yield {pubkey: e.pubkey, server: e.server, expiresAt: e.expiresAt, timestamp: e.timestamp}
452464 let affectedKeys = union
453465 (do
454466 e <- onlineEntries
@@ -500,7 +512,7 @@ serve = \req -> case req of
500512 o <- offlineEntries
501513 where o.pubkey == k.pubkey
502514 where o.server == k.server
503− where o.timestamp >= p.expiresAt - maxPresenceTtl * 1000
515+ where o.timestamp >= p.timestamp
504516 yield o) > 0
505517 yield p)
506518 set *presence = if validRequest
@@ -508,6 +520,7 @@ serve = \req -> case req of
508520 else presence
509521 let validEvents = do
510522 e <- events
523+ where validRequest
511524 where not (e.server == "")
512525 where not (e.pubkey == "")
513526 where not (e.server == serverHost)
@@ -540,7 +553,7 @@ serve = \req -> case req of
540553 t <- now
541554 let authorized = verifyFederationAuth authorization
542555 let validOrigin = not (originServer == "") && not (originServer == serverHost)
543− let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "")
556+ let validFields = not (toKey == "") && not (fromKey == "") && not (encryptedBlob == "") && not (messageId == "") && not (tooLong toKey maxPubkeyLen) && not (tooLong fromKey maxPubkeyLen) && not (tooLong encryptedBlob maxBlobLen)
544557 let clampedTs = if timestamp > t then t else if timestamp < t - messageMaxAge then t else timestamp
545558 atomic do
546559 pruneExpired t
@@ -563,7 +576,7 @@ serve = \req -> case req of
563576 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
564577 t <- now
565578 let authorized = verifyFederationAuth authorization
566− let validAck = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost)
579+ let validAck = authorized && originServer == serverHost && not (fromServer == "") && not (fromServer == serverHost) && count messageIds <= maxMessageIds
567580 atomic do
568581 pruneExpired t
569582 messages <- *messages