upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/nostr.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-28 15:25:42 -0300
committerGitHub <noreply@github.com>2026-07-28 15:25:42 -0300
commit8a613d756c25983588c6ccabf3752aa31a4e7cb2 (patch)
tree44bf87fdf0316dbe3670cbe6df5bc3a08993f0ec /src/nostr.cpp
parent8770e49d5264bdc9f6daec61dee74ca74dabdc0a (diff)
nostr: bound what a relay can make the node allocate (#17)
The node connects to public Nostr relays it does not run, and the WebSocket reader trusted them completely. A frame's payload length is read from the wire -- up to 64 bits when the extended form is used -- and passed straight to string::resize(). One frame header claiming 2^40 bytes is enough to make the node allocate until it dies. Reassembly across continuation frames had no ceiling either, so the same result was reachable with small frames. Frames are now capped at 1 MiB and a reassembled message at 4 MiB; the frame cap also keeps the length inside the int that ReadN takes, which a 64-bit value would otherwise have narrowed, possibly to a negative. Six message loops then did: if (!j.is_array() || j.empty()) continue; string type = j[0].get<string>(); j[0] was never checked to be a string, so a relay answering with [1] threw an uncaught json::type_error and took the thread down. Same shape as the descriptor bug fixed in #12, in six more places. Nothing here is consensus -- this is the relay-facing edge only.
Diffstat (limited to 'src/nostr.cpp')
-rw-r--r--src/nostr.cpp27
1 files changed, 21 insertions, 6 deletions
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 009edd1..6f3b6a7 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -398,6 +398,12 @@ static bool BuildSignedEvent(CNostrKey& key, int kind, const json& tags,
398// 398//
399// Minimal WebSocket client over TLS (wss) or TCP (ws), blocking with timeout. 399// Minimal WebSocket client over TLS (wss) or TCP (ws), blocking with timeout.
400// 400//
401// Ceilings on what a relay may make this node allocate. Nostr events are
402// small -- relays themselves usually refuse anything over a few hundred KB --
403// so these are generous, and they bound an otherwise attacker-chosen size.
404static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame
405static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled
406
401class CWebSocket 407class CWebSocket
402{ 408{
403public: 409public:
@@ -597,6 +603,14 @@ public:
597 len = 0; 603 len = 0;
598 for (int i = 0; i < 8; i++) len = (len << 8) | e[i]; 604 for (int i = 0; i < 8; i++) len = (len << 8) | e[i];
599 } 605 }
606 // The length is whatever the relay says it is, and these are public
607 // relays the node does not control. Unbounded, a single frame header
608 // claiming 2^40 bytes makes the node allocate until it dies -- and
609 // reassembly across continuation frames grows without limit even
610 // when each frame is small. Cap both, and refuse anything that will
611 // not survive the int narrowing ReadN takes.
612 if (len > MAX_WS_FRAME) return false;
613
600 unsigned char mask[4] = {0,0,0,0}; 614 unsigned char mask[4] = {0,0,0,0};
601 if (masked && !ReadN((char*)mask, 4)) return false; 615 if (masked && !ReadN((char*)mask, 4)) return false;
602 616
@@ -610,6 +624,7 @@ public:
610 if (opcode == 0x8) return false; // close 624 if (opcode == 0x8) return false; // close
611 if (opcode == 0x9) { SendPong(payload); continue; } // ping 625 if (opcode == 0x9) { SendPong(payload); continue; } // ping
612 if (opcode == 0xA) continue; // pong 626 if (opcode == 0xA) continue; // pong
627 if (out.size() + payload.size() > MAX_WS_MESSAGE) return false;
613 out += payload; // text/continuation 628 out += payload; // text/continuation
614 if (fin) return true; 629 if (fin) return true;
615 } 630 }
@@ -847,7 +862,7 @@ bool BtfQueryPoolAnnouncement(const std::string& poolBtfAddr, BtfPoolAnnouncemen
847 break; 862 break;
848 json j; 863 json j;
849 try { j = json::parse(msg); } catch (...) { continue; } 864 try { j = json::parse(msg); } catch (...) { continue; }
850 if (!j.is_array() || j.empty()) continue; 865 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
851 string type = j[0].get<string>(); 866 string type = j[0].get<string>();
852 if (type == "EVENT" && j.size() >= 3) 867 if (type == "EVENT" && j.size() >= 3)
853 { 868 {
@@ -957,7 +972,7 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
957 break; 972 break;
958 json j; 973 json j;
959 try { j = json::parse(msg); } catch (...) { continue; } 974 try { j = json::parse(msg); } catch (...) { continue; }
960 if (!j.is_array() || j.empty()) continue; 975 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
961 string t = j[0].get<string>(); 976 string t = j[0].get<string>();
962 if (t == "EVENT" && j.size() >= 3) 977 if (t == "EVENT" && j.size() >= 3)
963 { 978 {
@@ -1068,7 +1083,7 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs,
1068 break; 1083 break;
1069 json j; 1084 json j;
1070 try { j = json::parse(msg); } catch (...) { continue; } 1085 try { j = json::parse(msg); } catch (...) { continue; }
1071 if (!j.is_array() || j.empty()) continue; 1086 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1072 string t = j[0].get<string>(); 1087 string t = j[0].get<string>();
1073 if (t == "EVENT" && j.size() >= 3) 1088 if (t == "EVENT" && j.size() >= 3)
1074 { 1089 {
@@ -1144,7 +1159,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
1144 break; 1159 break;
1145 json j; 1160 json j;
1146 try { j = json::parse(msg); } catch (...) { continue; } 1161 try { j = json::parse(msg); } catch (...) { continue; }
1147 if (!j.is_array() || j.empty()) continue; 1162 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1148 string type = j[0].get<string>(); 1163 string type = j[0].get<string>();
1149 if (type == "EVENT" && j.size() >= 3) 1164 if (type == "EVENT" && j.size() >= 3)
1150 { 1165 {
@@ -1194,7 +1209,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
1194 break; 1209 break;
1195 json j; 1210 json j;
1196 try { j = json::parse(msg); } catch (...) { continue; } 1211 try { j = json::parse(msg); } catch (...) { continue; }
1197 if (!j.is_array() || j.empty()) continue; 1212 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1198 string type = j[0].get<string>(); 1213 string type = j[0].get<string>();
1199 if (type == "EVENT" && j.size() >= 3) 1214 if (type == "EVENT" && j.size() >= 3)
1200 { 1215 {
@@ -1234,7 +1249,7 @@ static bool SeedFromRelay(CNostrKey& key, const string& relay)
1234 break; 1249 break;
1235 json j; 1250 json j;
1236 try { j = json::parse(msg); } catch (...) { continue; } 1251 try { j = json::parse(msg); } catch (...) { continue; }
1237 if (!j.is_array() || j.empty()) continue; 1252 if (!j.is_array() || j.empty() || !j[0].is_string()) continue;
1238 string type = j[0].get<string>(); 1253 string type = j[0].get<string>();
1239 if (type == "EVENT" && j.size() >= 3) 1254 if (type == "EVENT" && j.size() >= 3)
1240 HandlePoolAnnouncement(j[2]); 1255 HandlePoolAnnouncement(j[2]);