diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-19 18:17:06 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-08-23 04:37:36 +0000 |
| commit | a7744058c49b1ba2c605f9e8380d9c3e8665bbc0 (patch) | |
| tree | c0128ca95c6d761689e299c64e5d27b86f1c5e32 /src/main.cpp | |
| parent | ee7613be2f1d679c7ea6e3948c44110313d230b1 (diff) | |
net: stop serving the 2009 marketplace to anonymous peers
Four inherited handlers took attacker-controlled input and wrote to disk
with nothing authorising it. None of them had a caller left anywhere in
the tree, so all four were reachable only from the outside.
checkorder called GenerateNewKey() -- AddKey(), a wallet.dat write -- for
any peer that asked, and handed back the pubkey. Its comment claims the
cache keeps one key per ip, and on a 2009 network of routable addresses
it did. Not here: inbound peers arrive through a rendezvous tunnel and
are tagged by BtfMarkerAddr() over three random bytes, so addr.ip is
fresh every connection and the cache never hits. Reconnect in a loop and
the node mints a key per connection, none of them in any backup taken
beforehand, which for a pre-BIP32 wallet is how coins go missing. The map
holding them was static and never trimmed.
submitorder AddToWallet()'d a CWalletTx from any peer that sent one. It
had to be a valid transaction; it did not have to be ours.
review checked a signature the sender made with its own key, which proves
possession and authorises nothing, then appended to reviews.dat by
reading the whole vector, pushing one entry and writing it all back --
quadratic disk on an unbounded map<string,string> off the socket -- then
walked a link graph the same peer supplied and relayed the message to
every other node.
reply dispatched into CRequestTracker. Nothing ever called PushRequest,
so the table was permanently empty and the handler was a parser and
nothing else.
Removed with the subsystem behind them: market.cpp, CProduct, CReview,
CUser, CReviewDB, the advert publish templates and the subscribe channel
machinery, none of which had an inbound handler either. MSG_REVIEW,
MSG_PRODUCT and MSG_TABLE stay in the inv enum, unserved: CInv puts type
on the wire as an integer and MSG_TX and MSG_BLOCK are shared with every
other node.
Unknown commands are already ignored for extensibility, so peers still
running these lose nothing but an answer they never asked for. Verified
by a clean build of bitflash-node.
(cherry picked from commit ad9c393866c007a115f32d70a9017ce91066a7d1)
Diffstat (limited to 'src/main.cpp')
| -rw-r--r-- | src/main.cpp | 115 |
1 files changed, 37 insertions, 78 deletions
diff --git a/src/main.cpp b/src/main.cpp index 62068d2..efff96c 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -2782,9 +2782,10 @@ bool AlreadyHave(CTxDB& txdb, const CInv& inv) | |||
| 2782 | { | 2782 | { |
| 2783 | case MSG_TX: return mapTransactions.count(inv.hash) || txdb.ContainsTx(inv.hash); | 2783 | case MSG_TX: return mapTransactions.count(inv.hash) || txdb.ContainsTx(inv.hash); |
| 2784 | case MSG_BLOCK: return mapBlockIndex.count(inv.hash) || mapOrphanBlocks.count(inv.hash); | 2784 | case MSG_BLOCK: return mapBlockIndex.count(inv.hash) || mapOrphanBlocks.count(inv.hash); |
| 2785 | case MSG_REVIEW: return true; | ||
| 2786 | case MSG_PRODUCT: return mapProducts.count(inv.hash); | ||
| 2787 | } | 2785 | } |
| 2786 | // MSG_REVIEW / MSG_PRODUCT / MSG_TABLE are the 2009 marketplace inventory | ||
| 2787 | // types. Nothing announces or serves them any more (see ProcessMessage), | ||
| 2788 | // so they fall through to the default and are never requested. | ||
| 2788 | // Don't know what it is, just say we already got one | 2789 | // Don't know what it is, just say we already got one |
| 2789 | return true; | 2790 | return true; |
| 2790 | } | 2791 | } |
| @@ -2942,7 +2943,6 @@ bool ProcessMessages(CNode* pfrom) | |||
| 2942 | 2943 | ||
| 2943 | bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | 2944 | bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) |
| 2944 | { | 2945 | { |
| 2945 | static map<unsigned int, vector<unsigned char> > mapReuseKey; | ||
| 2946 | if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), (int)vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } | 2946 | if (LogAcceptsCategory("net")) { printf("received: %-12s (%d bytes) ", strCommand.c_str(), (int)vRecv.size()); for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++) printf("%02x ", vRecv[i] & 0xff); printf("\n"); } |
| 2947 | if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) | 2947 | if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0) |
| 2948 | { | 2948 | { |
| @@ -3181,22 +3181,18 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 3181 | } | 3181 | } |
| 3182 | 3182 | ||
| 3183 | 3183 | ||
| 3184 | else if (strCommand == "review") | 3184 | // "review" used to be handled here. It took a CReview off the wire from an |
| 3185 | { | 3185 | // unauthenticated peer, checked a signature the sender had made with a key |
| 3186 | CDataStream vMsg(vRecv); | 3186 | // of its own choosing -- which proves possession of that key and authorises |
| 3187 | CReview review; | 3187 | // nothing -- and then appended the message to reviews.dat by reading the |
| 3188 | vRecv >> review; | 3188 | // whole vector for the target hash, pushing one entry, and writing all of |
| 3189 | 3189 | // it back. That is quadratic disk for a stream an attacker controls, on a | |
| 3190 | CInv inv(MSG_REVIEW, review.GetHash()); | 3190 | // record whose mapValue is an unbounded map<string,string> read straight |
| 3191 | pfrom->AddInventoryKnown(inv); | 3191 | // from the socket, followed by AddAtomsAndPropagate walking a link graph |
| 3192 | 3192 | // the same peer supplied, and finally RelayMessage forwarding it to every | |
| 3193 | if (review.AcceptReview()) | 3193 | // other node. Unbounded remote disk growth with network amplification and |
| 3194 | { | 3194 | // no authorisation anywhere in the path. The marketplace it belonged to has |
| 3195 | // Relay the original message as-is in case it's a higher version than we know how to parse | 3195 | // not existed since 2009; the handler is gone rather than bounded. |
| 3196 | RelayMessage(inv, vMsg); | ||
| 3197 | mapAlreadyAskedFor.erase(inv); | ||
| 3198 | } | ||
| 3199 | } | ||
| 3200 | 3196 | ||
| 3201 | 3197 | ||
| 3202 | else if (strCommand == "block") | 3198 | else if (strCommand == "block") |
| @@ -3229,65 +3225,28 @@ bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv) | |||
| 3229 | } | 3225 | } |
| 3230 | 3226 | ||
| 3231 | 3227 | ||
| 3232 | else if (strCommand == "checkorder") | 3228 | // "checkorder", "submitorder" and "reply" -- the 2009 buy-from-a-merchant |
| 3233 | { | 3229 | // exchange -- used to be handled here. All three are gone. |
| 3234 | uint256 hashReply; | 3230 | // |
| 3235 | CWalletTx order; | 3231 | // "checkorder" called GenerateNewKey() for any peer that asked, which is |
| 3236 | vRecv >> hashReply >> order; | 3232 | // AddKey() and a wallet.dat write, and handed the pubkey back. The comment |
| 3237 | 3233 | // said "keep giving the same key to the same ip until they use it", and on | |
| 3238 | /// we have a chance to check the order here | 3234 | // a 2009 network of routable IPs that held. It does not hold here: every |
| 3239 | 3235 | // inbound peer arrives through a rendezvous tunnel and is tagged with | |
| 3240 | // Keep giving the same key to the same ip until they use it | 3236 | // BtfMarkerAddr() over three random bytes (net.cpp), so addr.ip is freshly |
| 3241 | if (!mapReuseKey.count(pfrom->addr.ip)) | 3237 | // random per connection and the cache never hits. Reconnect in a loop and |
| 3242 | mapReuseKey[pfrom->addr.ip] = GenerateNewKey(); | 3238 | // the node mints a key per connection -- unbounded wallet growth, unbounded |
| 3243 | 3239 | // growth of the static map that held them, and every one of those keys | |
| 3244 | // Send back approval of order and pubkey to use | 3240 | // missing from every backup taken beforehand, which for this pre-BIP32 |
| 3245 | CScript scriptPubKey; | 3241 | // wallet is how coins are lost. |
| 3246 | scriptPubKey << mapReuseKey[pfrom->addr.ip] << OP_CHECKSIG; | 3242 | // |
| 3247 | pfrom->PushMessage("reply", hashReply, (int)0, scriptPubKey); | 3243 | // "submitorder" took a CWalletTx from an unauthenticated peer and, if it |
| 3248 | } | 3244 | // was merely a valid transaction, AddToWallet()'d it. Valid is not the same |
| 3249 | 3245 | // as ours: any peer could write chosen transactions into wallet.dat. | |
| 3250 | 3246 | // | |
| 3251 | else if (strCommand == "submitorder") | 3247 | // "reply" dispatched to a CRequestTracker callback. Nothing in this tree |
| 3252 | { | 3248 | // has ever called PushRequest, so mapRequests was permanently empty and the |
| 3253 | uint256 hashReply; | 3249 | // handler could only ever be a parser for hostile input. |
| 3254 | CWalletTx wtxNew; | ||
| 3255 | vRecv >> hashReply >> wtxNew; | ||
| 3256 | |||
| 3257 | // Broadcast | ||
| 3258 | if (!wtxNew.AcceptWalletTransaction()) | ||
| 3259 | { | ||
| 3260 | pfrom->PushMessage("reply", hashReply, (int)1); | ||
| 3261 | return error("submitorder AcceptWalletTransaction() failed, returning error 1"); | ||
| 3262 | } | ||
| 3263 | wtxNew.fTimeReceivedIsTxTime = true; | ||
| 3264 | AddToWallet(wtxNew); | ||
| 3265 | wtxNew.RelayWalletTransaction(); | ||
| 3266 | mapReuseKey.erase(pfrom->addr.ip); | ||
| 3267 | |||
| 3268 | // Send back confirmation | ||
| 3269 | pfrom->PushMessage("reply", hashReply, (int)0); | ||
| 3270 | } | ||
| 3271 | |||
| 3272 | |||
| 3273 | else if (strCommand == "reply") | ||
| 3274 | { | ||
| 3275 | uint256 hashReply; | ||
| 3276 | vRecv >> hashReply; | ||
| 3277 | |||
| 3278 | CRequestTracker tracker; | ||
| 3279 | CRITICAL_BLOCK(pfrom->cs_mapRequests) | ||
| 3280 | { | ||
| 3281 | map<uint256, CRequestTracker>::iterator mi = pfrom->mapRequests.find(hashReply); | ||
| 3282 | if (mi != pfrom->mapRequests.end()) | ||
| 3283 | { | ||
| 3284 | tracker = (*mi).second; | ||
| 3285 | pfrom->mapRequests.erase(mi); | ||
| 3286 | } | ||
| 3287 | } | ||
| 3288 | if (!tracker.IsNull()) | ||
| 3289 | tracker.fn(tracker.param1, vRecv); | ||
| 3290 | } | ||
| 3291 | 3250 | ||
| 3292 | 3251 | ||
| 3293 | else if (strCommand == "ping") | 3252 | else if (strCommand == "ping") |