From cf20311af5a6affee03a35736ef2b609d9c43f5a Mon Sep 17 00:00:00 2001 From: MMGen Date: Sat, 3 Mar 2018 07:00:42 +0000 Subject: [PATCH] tx: max_tx_file_size, additional checks during parsing Since the tx file is potentially untrusted data, be strict about what we allow: - check data size against max_tx_file_size (defaults to 100000 bytes) - check for ascii encoding - check line lengths - reject extraneous keys in inputs and outputs (done by MMGenListItem) max_tx_file_size can be overridden in config file --- data_files/mmgen.cfg | 3 +++ mmgen/globalvars.py | 4 +++- mmgen/tx.py | 29 ++++++++++++++++++----------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/data_files/mmgen.cfg b/data_files/mmgen.cfg index 58c147de..235e4b02 100644 --- a/data_files/mmgen.cfg +++ b/data_files/mmgen.cfg @@ -59,6 +59,9 @@ # multiplied by this value: # tx_fee_adj 1.0 +# Set the maximum transaction file size: +# max_tx_file_size 100000 + ##################################################################### # The following options are probably of interest only to developers # ##################################################################### diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index cfbb7c4c..01402493 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -130,7 +130,8 @@ class g(object): 'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port', 'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password', 'daemon_data_dir','force_256_color','regtest', - 'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee' + 'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee', + 'max_tx_file_size' ) env_opts = ( 'MMGEN_BOGUS_WALLET_DATA', @@ -148,6 +149,7 @@ class g(object): min_screen_width = 80 minconf = 1 + max_tx_file_size = 100000 # Global var sets user opt: global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug', diff --git a/mmgen/tx.py b/mmgen/tx.py index 1e6271ce..9481a4fe 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -507,14 +507,6 @@ class MMGenTX(MMGenObject): for e in getattr(self,desc): if hasattr(e,attr): delattr(e,attr) - def decode_io(self,desc,data): - io,il = ( - (MMGenTX.MMGenTxOutput,MMGenTX.MMGenTxOutputList), - (MMGenTX.MMGenTxInput,MMGenTX.MMGenTxInputList) - )[desc=='inputs'] - return il([io(**dict([(k,d[k]) for k in io.__dict__ - if k in d and d[k] not in ('',None)])) for d in data]) - def decode_io_oldfmt(self,data): tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'} tr_rev = dict([(v,k) for k,v in tr.items()]) @@ -581,6 +573,9 @@ class MMGenTX(MMGenObject): self.chksum = make_chksum_6(' '.join(lines)) self.fmt_data = '\n'.join([self.chksum] + lines)+'\n' + assert len(self.fmt_data) <= g.max_tx_file_size,( + 'Transaction file size exceeds limit ({} bytes)'.format(g.max_tx_file_size)) + def get_non_mmaddrs(self,desc): return list(set(i.addr for i in getattr(self,desc) if not i.mmid)) @@ -971,8 +966,6 @@ class MMGenTX(MMGenObject): def parse_tx_file(self,infile,md_only=False,silent_open=False): - tx_data = get_lines_from_file(infile,self.desc+' data',silent=silent_open) - def eval_io_data(raw_data,desc): from ast import literal_eval try: @@ -985,19 +978,32 @@ class MMGenTX(MMGenObject): assert type(d) == list,'{} data not a list!'.format(desc) assert len(d),'no {}!'.format(desc) for e in d: e['amt'] = g.proto.coin_amt(e['amt']) - return self.decode_io(desc,d) + io,io_list = ( + (MMGenTX.MMGenTxOutput,MMGenTX.MMGenTxOutputList), + (MMGenTX.MMGenTxInput,MMGenTX.MMGenTxInputList) + )[desc=='inputs'] + return io_list([io(**e) for e in d]) + + tx_data = get_data_from_file(infile,self.desc+' data',silent=silent_open) try: desc = 'data' + assert len(tx_data) <= g.max_tx_file_size,( + 'Transaction file size exceeds limit ({} bytes)'.format(g.max_tx_file_size)) + tx_data = tx_data.decode('ascii').splitlines() assert len(tx_data) >= 5,'number of lines less than 5' + assert len(tx_data[0]) == 6,'invalid length of first line' self.chksum = HexStr(tx_data.pop(0),on_fail='raise') assert self.chksum == make_chksum_6(' '.join(tx_data)),'file data does not match checksum' if len(tx_data) == 6: + assert len(tx_data[-1]) == 64,'invalid coin TxID length' desc = '{} TxID'.format(g.proto.name.capitalize()) self.coin_txid = CoinTxID(tx_data.pop(-1),on_fail='raise') if len(tx_data) == 5: + # rough check: allow for 4-byte utf8 characters + base58 (4 * 11 / 8 = 6 (rounded up)) + assert len(tx_data[-1]) < MMGenTXLabel.max_len*6,'invalid comment length' c = tx_data.pop(-1) if c != '-': desc = 'encoded comment (not base58)' @@ -1008,6 +1014,7 @@ class MMGenTX(MMGenObject): desc = 'number of lines' # four required lines metadata,self.hex,inputs_data,outputs_data = tx_data + assert len(metadata) < 60,'invalid metadata length' # rough check metadata = metadata.split() if metadata[-1].find('LT=') == 0: