diff options
| author | Bitflash <btf01@proton.me> | 2026-07-28 15:25:42 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-28 15:25:42 -0300 |
| commit | 8a613d756c25983588c6ccabf3752aa31a4e7cb2 (patch) | |
| tree | 44bf87fdf0316dbe3670cbe6df5bc3a08993f0ec /src/nostr.cpp | |
| parent | 8770e49d5264bdc9f6daec61dee74ca74dabdc0a (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.cpp | 27 |
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. | ||
| 404 | static const uint64 MAX_WS_FRAME = 1 << 20; // 1 MiB per frame | ||
| 405 | static const uint64 MAX_WS_MESSAGE = 4 << 20; // 4 MiB reassembled | ||
| 406 | |||
| 401 | class CWebSocket | 407 | class CWebSocket |
| 402 | { | 408 | { |
| 403 | public: | 409 | public: |
| @@ -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]); |