[REST] remove json input for getutxos, limit to query max. 15 outpoints

Remove possibility to send json encoded parameters to `/rest/getutxos/` to avoid possible DoS scenarios.

The JSON output option is untouched.
This commit is contained in:
Jonas Schnelli 2015-05-27 15:56:16 +02:00
parent 64b8027c5c
commit 6e71efa9f0
3 changed files with 108 additions and 92 deletions

View File

@ -47,7 +47,7 @@ Only supports JSON as output format.
* chainwork : (string) total amount of work in active chain, in hexadecimal * chainwork : (string) total amount of work in active chain, in hexadecimal
####Query UTXO set ####Query UTXO set
`GET /rest/getutxos.<bin|hex|json>` `GET /rest/getutxos/<checkmempool>/<txid>-<n>/<txid>-<n>/.../<txid>-<n>.<bin|hex|json>`
The getutxo command allows querying of the UTXO set given a set of outpoints. The getutxo command allows querying of the UTXO set given a set of outpoints.
See BIP64 for input and output serialisation: See BIP64 for input and output serialisation:
@ -55,7 +55,7 @@ https://github.com/bitcoin/bips/blob/master/bip-0064.mediawiki
Example: Example:
``` ```
$ curl --data '{"checkmempool":true,"outpoints":[{"txid":"b2cdfd7b89def827ff8af7cd9bff7627ff72e5e8b0f71210f92ea7a4000c5d75","n":0}]}' localhost:18332/rest/getutxos.json 2>/dev/null | json_pp $ curl localhost:18332/rest/getutxos/checkmempool/b2cdfd7b89def827ff8af7cd9bff7627ff72e5e8b0f71210f92ea7a4000c5d75-0.json 2>/dev/null | json_pp
{ {
"chaintipHash" : "00000000fb01a7f3745a717f8caebee056c484e6e0bfe4a9591c235bb70506fb", "chaintipHash" : "00000000fb01a7f3745a717f8caebee056c484e6e0bfe4a9591c235bb70506fb",
"chainHeight" : 325347, "chainHeight" : 325347,

View File

@ -54,78 +54,78 @@ class RESTTest (BitcoinTestFramework):
connect_nodes_bi(self.nodes,0,2) connect_nodes_bi(self.nodes,0,2)
self.is_network_split=False self.is_network_split=False
self.sync_all() self.sync_all()
def run_test(self): def run_test(self):
url = urlparse.urlparse(self.nodes[0].url) url = urlparse.urlparse(self.nodes[0].url)
print "Mining blocks..." print "Mining blocks..."
self.nodes[0].generate(1) self.nodes[0].generate(1)
self.sync_all() self.sync_all()
self.nodes[2].generate(100) self.nodes[2].generate(100)
self.sync_all() self.sync_all()
assert_equal(self.nodes[0].getbalance(), 50) assert_equal(self.nodes[0].getbalance(), 50)
txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1) txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 0.1)
self.sync_all() self.sync_all()
self.nodes[2].generate(1) self.nodes[2].generate(1)
self.sync_all() self.sync_all()
bb_hash = self.nodes[0].getbestblockhash() bb_hash = self.nodes[0].getbestblockhash()
assert_equal(self.nodes[1].getbalance(), Decimal("0.1")) #balance now should be 0.1 on node 1 assert_equal(self.nodes[1].getbalance(), Decimal("0.1")) #balance now should be 0.1 on node 1
# load the latest 0.1 tx over the REST API # load the latest 0.1 tx over the REST API
json_string = http_get_call(url.hostname, url.port, '/rest/tx/'+txid+self.FORMAT_SEPARATOR+"json") json_string = http_get_call(url.hostname, url.port, '/rest/tx/'+txid+self.FORMAT_SEPARATOR+"json")
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
vintx = json_obj['vin'][0]['txid'] # get the vin to later check for utxo (should be spent by then) vintx = json_obj['vin'][0]['txid'] # get the vin to later check for utxo (should be spent by then)
# get n of 0.1 outpoint # get n of 0.1 outpoint
n = 0 n = 0
for vout in json_obj['vout']: for vout in json_obj['vout']:
if vout['value'] == 0.1: if vout['value'] == 0.1:
n = vout['n'] n = vout['n']
###################################### ######################################
# GETUTXOS: query a unspent outpoint # # GETUTXOS: query a unspent outpoint #
###################################### ######################################
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'}]}' json_request = '/checkmempool/'+txid+'-'+str(n)
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request) json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json')
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
#check chainTip response #check chainTip response
assert_equal(json_obj['chaintipHash'], bb_hash) assert_equal(json_obj['chaintipHash'], bb_hash)
#make sure there is one utxo #make sure there is one utxo
assert_equal(len(json_obj['utxos']), 1) assert_equal(len(json_obj['utxos']), 1)
assert_equal(json_obj['utxos'][0]['value'], 0.1) assert_equal(json_obj['utxos'][0]['value'], 0.1)
################################################ ################################################
# GETUTXOS: now query a already spent outpoint # # GETUTXOS: now query a already spent outpoint #
################################################ ################################################
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+vintx+'","n":0}]}' json_request = '/checkmempool/'+vintx+'-0'
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request) json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json')
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
#check chainTip response #check chainTip response
assert_equal(json_obj['chaintipHash'], bb_hash) assert_equal(json_obj['chaintipHash'], bb_hash)
#make sure there is no utox in the response because this oupoint has been spent #make sure there is no utox in the response because this oupoint has been spent
assert_equal(len(json_obj['utxos']), 0) assert_equal(len(json_obj['utxos']), 0)
#check bitmap #check bitmap
assert_equal(json_obj['bitmap'], "0") assert_equal(json_obj['bitmap'], "0")
################################################## ##################################################
# GETUTXOS: now check both with the same request # # GETUTXOS: now check both with the same request #
################################################## ##################################################
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'},{"txid":"'+vintx+'","n":0}]}' json_request = '/checkmempool/'+txid+'-'+str(n)+'/'+vintx+'-0'
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request) json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json')
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
assert_equal(len(json_obj['utxos']), 1) assert_equal(len(json_obj['utxos']), 1)
assert_equal(json_obj['bitmap'], "10") assert_equal(json_obj['bitmap'], "10")
#test binary response #test binary response
bb_hash = self.nodes[0].getbestblockhash() bb_hash = self.nodes[0].getbestblockhash()
@ -134,19 +134,18 @@ class RESTTest (BitcoinTestFramework):
binaryRequest += pack("i", n); binaryRequest += pack("i", n);
binaryRequest += binascii.unhexlify(vintx); binaryRequest += binascii.unhexlify(vintx);
binaryRequest += pack("i", 0); binaryRequest += pack("i", 0);
bin_response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'bin', binaryRequest) bin_response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'bin', binaryRequest)
output = StringIO.StringIO() output = StringIO.StringIO()
output.write(bin_response) output.write(bin_response)
output.seek(0) output.seek(0)
chainHeight = unpack("i", output.read(4))[0] chainHeight = unpack("i", output.read(4))[0]
hashFromBinResponse = hex(deser_uint256(output))[2:].zfill(65).rstrip("L") hashFromBinResponse = hex(deser_uint256(output))[2:].zfill(65).rstrip("L")
assert_equal(bb_hash, hashFromBinResponse) #check if getutxo's chaintip during calculation was fine assert_equal(bb_hash, hashFromBinResponse) #check if getutxo's chaintip during calculation was fine
assert_equal(chainHeight, 102) #chain height must be 102 assert_equal(chainHeight, 102) #chain height must be 102
############################ ############################
# GETUTXOS: mempool checks # # GETUTXOS: mempool checks #
############################ ############################
@ -156,55 +155,56 @@ class RESTTest (BitcoinTestFramework):
json_string = http_get_call(url.hostname, url.port, '/rest/tx/'+txid+self.FORMAT_SEPARATOR+"json") json_string = http_get_call(url.hostname, url.port, '/rest/tx/'+txid+self.FORMAT_SEPARATOR+"json")
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
vintx = json_obj['vin'][0]['txid'] # get the vin to later check for utxo (should be spent by then) vintx = json_obj['vin'][0]['txid'] # get the vin to later check for utxo (should be spent by then)
# get n of 0.1 outpoint # get n of 0.1 outpoint
n = 0 n = 0
for vout in json_obj['vout']: for vout in json_obj['vout']:
if vout['value'] == 0.1: if vout['value'] == 0.1:
n = vout['n'] n = vout['n']
json_request = '{"checkmempool":false,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'}]}' json_request = '/'+txid+'-'+str(n)
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request) json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json')
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
assert_equal(len(json_obj['utxos']), 0) #there should be a outpoint because it has just added to the mempool assert_equal(len(json_obj['utxos']), 0) #there should be a outpoint because it has just added to the mempool
json_request = '{"checkmempool":true,"outpoints":[{"txid":"'+txid+'","n":'+str(n)+'}]}' json_request = '/checkmempool/'+txid+'-'+str(n)
json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request) json_string = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json')
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
assert_equal(len(json_obj['utxos']), 1) #there should be a outpoint because it has just added to the mempool assert_equal(len(json_obj['utxos']), 1) #there should be a outpoint because it has just added to the mempool
#do some invalid requests #do some invalid requests
json_request = '{"checkmempool' json_request = '{"checkmempool'
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True) response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True)
assert_equal(response.status, 500) #must be a 500 because we send a invalid json request assert_equal(response.status, 500) #must be a 500 because we send a invalid json request
json_request = '{"checkmempool' json_request = '{"checkmempool'
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'bin', json_request, True) response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'bin', json_request, True)
assert_equal(response.status, 500) #must be a 500 because we send a invalid bin request assert_equal(response.status, 500) #must be a 500 because we send a invalid bin request
response = http_get_call(url.hostname, url.port, '/rest/getutxos/checkmempool'+self.FORMAT_SEPARATOR+'bin', '', True)
assert_equal(response.status, 500) #must be a 500 because we send a invalid bin request
#test limits #test limits
json_request = '{"checkmempool":true,"outpoints":[' json_request = '/checkmempool/'
for x in range(0, 200): for x in range(0, 20):
json_request += '{"txid":"'+txid+'","n":'+str(n)+'},' json_request += txid+'-'+str(n)+'/'
json_request = json_request.rstrip(",") json_request = json_request.rstrip("/")
json_request+="]}"; response = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json', '', True)
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True)
assert_equal(response.status, 500) #must be a 500 because we exceeding the limits assert_equal(response.status, 500) #must be a 500 because we exceeding the limits
json_request = '{"checkmempool":true,"outpoints":[' json_request = '/checkmempool/'
for x in range(0, 90): for x in range(0, 15):
json_request += '{"txid":"'+txid+'","n":'+str(n)+'},' json_request += txid+'-'+str(n)+'/'
json_request = json_request.rstrip(",") json_request = json_request.rstrip("/");
json_request+="]}"; response = http_get_call(url.hostname, url.port, '/rest/getutxos'+json_request+self.FORMAT_SEPARATOR+'json', '', True)
response = http_get_call(url.hostname, url.port, '/rest/getutxos'+self.FORMAT_SEPARATOR+'json', json_request, True)
assert_equal(response.status, 200) #must be a 500 because we exceeding the limits assert_equal(response.status, 200) #must be a 500 because we exceeding the limits
self.nodes[0].generate(1) #generate block to not affect upcomming tests self.nodes[0].generate(1) #generate block to not affect upcomming tests
self.sync_all() self.sync_all()
################ ################
# /rest/block/ # # /rest/block/ #
################ ################
# check binary format # check binary format
response = http_get_call(url.hostname, url.port, '/rest/block/'+bb_hash+self.FORMAT_SEPARATOR+"bin", "", True) response = http_get_call(url.hostname, url.port, '/rest/block/'+bb_hash+self.FORMAT_SEPARATOR+"bin", "", True)
assert_equal(response.status, 200) assert_equal(response.status, 200)
@ -248,7 +248,7 @@ class RESTTest (BitcoinTestFramework):
hex_string = http_get_call(url.hostname, url.port, '/rest/tx/'+tx_hash+self.FORMAT_SEPARATOR+"hex", "", True) hex_string = http_get_call(url.hostname, url.port, '/rest/tx/'+tx_hash+self.FORMAT_SEPARATOR+"hex", "", True)
assert_equal(hex_string.status, 200) assert_equal(hex_string.status, 200)
assert_greater_than(int(response.getheader('content-length')), 10) assert_greater_than(int(response.getheader('content-length')), 10)
# check block tx details # check block tx details
@ -278,7 +278,7 @@ class RESTTest (BitcoinTestFramework):
#test rest bestblock #test rest bestblock
bb_hash = self.nodes[0].getbestblockhash() bb_hash = self.nodes[0].getbestblockhash()
json_string = http_get_call(url.hostname, url.port, '/rest/chaininfo.json') json_string = http_get_call(url.hostname, url.port, '/rest/chaininfo.json')
json_obj = json.loads(json_string) json_obj = json.loads(json_string)
assert_equal(json_obj['bestblockhash'], bb_hash) assert_equal(json_obj['bestblockhash'], bb_hash)

View File

@ -19,7 +19,7 @@
using namespace std; using namespace std;
using namespace json_spirit; using namespace json_spirit;
static const int MAX_GETUTXOS_OUTPOINTS = 100; //allow a max of 100 outpoints to be queried at once static const int MAX_GETUTXOS_OUTPOINTS = 15; //allow a max of 15 outpoints to be queried at once
enum RetFormat { enum RetFormat {
RF_UNDEF, RF_UNDEF,
@ -342,16 +342,51 @@ static bool rest_getutxos(AcceptedConnection* conn,
vector<string> params; vector<string> params;
enum RetFormat rf = ParseDataFormat(params, strURIPart); enum RetFormat rf = ParseDataFormat(params, strURIPart);
vector<string> uriParts;
if (params.size() > 0 && params[0].length() > 1)
{
std::string strUriParams = params[0].substr(1);
boost::split(uriParts, strUriParams, boost::is_any_of("/"));
}
// throw exception in case of a empty request // throw exception in case of a empty request
if (strRequest.length() == 0) if (strRequest.length() == 0 && uriParts.size() == 0)
throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Error: empty request"); throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Error: empty request");
bool fInputParsed = false;
bool fCheckMemPool = false; bool fCheckMemPool = false;
vector<COutPoint> vOutPoints; vector<COutPoint> vOutPoints;
// parse/deserialize input // parse/deserialize input
// input-format = output-format, rest/getutxos/bin requires binary input, gives binary output, ... // input-format = output-format, rest/getutxos/bin requires binary input, gives binary output, ...
if (uriParts.size() > 0)
{
//inputs is sent over URI scheme (/rest/getutxos/checkmempool/txid1-n/txid2-n/...)
if (uriParts.size() > 0 && uriParts[0] == "checkmempool")
fCheckMemPool = true;
for (size_t i = (fCheckMemPool) ? 1 : 0; i < uriParts.size(); i++)
{
uint256 txid;
int32_t nOutput;
std::string strTxid = uriParts[i].substr(0, uriParts[i].find("-"));
std::string strOutput = uriParts[i].substr(uriParts[i].find("-")+1);
if (!ParseInt32(strOutput, &nOutput) || !IsHex(strTxid))
throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Parse error");
txid.SetHex(strTxid);
vOutPoints.push_back(COutPoint(txid, (uint32_t)nOutput));
}
if (vOutPoints.size() > 0)
fInputParsed = true;
else
throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Error: empty request");
}
string strRequestMutable = strRequest; //convert const string to string for allowing hex to bin converting string strRequestMutable = strRequest; //convert const string to string for allowing hex to bin converting
switch (rf) { switch (rf) {
@ -363,11 +398,17 @@ static bool rest_getutxos(AcceptedConnection* conn,
case RF_BINARY: { case RF_BINARY: {
try { try {
//deserialize //deserialize only if user sent a request
CDataStream oss(SER_NETWORK, PROTOCOL_VERSION); if (strRequestMutable.size() > 0)
oss << strRequestMutable; {
oss >> fCheckMemPool; if (fInputParsed) //don't allow sending input over URI and HTTP RAW DATA
oss >> vOutPoints; throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Combination of URI scheme inputs and raw post data is not allowed");
CDataStream oss(SER_NETWORK, PROTOCOL_VERSION);
oss << strRequestMutable;
oss >> fCheckMemPool;
oss >> vOutPoints;
}
} catch (const std::ios_base::failure& e) { } catch (const std::ios_base::failure& e) {
// abort in case of unreadable binary data // abort in case of unreadable binary data
throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Parse error"); throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Parse error");
@ -376,33 +417,8 @@ static bool rest_getutxos(AcceptedConnection* conn,
} }
case RF_JSON: { case RF_JSON: {
try { if (!fInputParsed)
// parse json request throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Error: empty request");
Value valRequest;
if (!read_string(strRequest, valRequest))
throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Parse error");
Object jsonObject = valRequest.get_obj();
const Value& checkMempoolValue = find_value(jsonObject, "checkmempool");
if (!checkMempoolValue.is_null()) {
fCheckMemPool = checkMempoolValue.get_bool();
}
const Value& outpointsValue = find_value(jsonObject, "outpoints");
if (!outpointsValue.is_null()) {
Array outPoints = outpointsValue.get_array();
BOOST_FOREACH (const Value& outPoint, outPoints) {
Object outpointObject = outPoint.get_obj();
uint256 txid = ParseHashO(outpointObject, "txid");
Value nValue = find_value(outpointObject, "n");
int nOutput = nValue.get_int();
vOutPoints.push_back(COutPoint(txid, nOutput));
}
}
} catch (...) {
// return HTTP 500 if there was a json parsing error
throw RESTERR(HTTP_INTERNAL_SERVER_ERROR, "Parse error");
}
break; break;
} }
default: { default: {