Browse Source

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
MMGen 7 years ago
parent
commit
cf20311af5
3 changed files with 24 additions and 12 deletions
  1. 3 0
      data_files/mmgen.cfg
  2. 3 1
      mmgen/globalvars.py
  3. 18 11
      mmgen/tx.py

+ 3 - 0
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 #
 #####################################################################

+ 3 - 1
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',

+ 18 - 11
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: