From 79dec74dcf225e9a8978d1140b2ce61b62c6d45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Mon, 15 Jul 2019 22:57:54 +0300 Subject: [PATCH] Validate input encoding --- ethermess.py | 103 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 18 deletions(-) diff --git a/ethermess.py b/ethermess.py index 9f4b18b..488d576 100644 --- a/ethermess.py +++ b/ethermess.py @@ -123,6 +123,54 @@ def format_status(status): else: raise ValueError('Unknown status %i' % status) +class NonCharacterError(Exception): pass +class ControlCharacterError(Exception): pass + +def validate_encoding(text, newline_allowed): + # We assume text is valid unicode, so we skip stuff like surrogate pairs + + for char in text: + codepoint = ord(char) + # Reject non-characters + if codepoint & 0xffff in [0xfffe, 0xffff]: + # Plane end non-character + raise NonCharacterError(char) + elif 0xfdd0 <= codepoint <= 0xfdef: + # BMP non-character block + raise NonCharacterError(char) + + # Reject control characters + if codepoint <= 0x1f: + # C0 control characters + if not newline_allowed or codepoint != 0x0a: + raise ControlCharacterError(char) + elif 0x80 <= codepoint <= 0x9f: + # C1 control characters + raise ControlCharacterError(char) + elif codepoint in [0x2028, 0x2029]: + # U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR + raise ControlCharacterError(char) + +class NickLengthError(Exception): pass + +def validate_nick(nick): + validate_encoding(nick, newline_allowed = False) + # Nick length is stored as one byte + if len(nick.encode('utf-8')) > 255: + raise NickLengthError + +class MessageLengthError(Exception): pass + +def validate_message(message): + validate_encoding(message, newline_allowed = True) + # Maximum frame payload is 1500 bytes + # -2 for EtherMess packet header + # -2 for msgid + # -2 for message length + # = 1494 + if len(message.encode('utf-8')) > 1494: + raise MessageLengthError + def send_message(backend, mac, message): encoded = message.encode('utf-8') writeall(backend, b'm' + mac + bytes([len(encoded) >> 8, len(encoded) & 0xff]) + encoded) @@ -131,6 +179,8 @@ def queue_message(backend, mac, message): global send_queue global next_queue_id + validate_message(message) + if len(send_queue) == 0: # Nothing being processed atm, send directly send_message(backend, mac, message) @@ -158,13 +208,12 @@ def handle_user_command(backend, line): global own_nick, own_status global default_target_mac - if len(line) > 0 and line[0] == '/': - command, _, rest = line.partition(' ') + try: + if len(line) > 0 and line[0] == '/': + command, _, rest = line.partition(' ') - try: if command == '/': # Send quoted message - # TODO: Validate message if default_target_mac is None: print('--- Default target not set, set with /target') else: @@ -201,7 +250,8 @@ def handle_user_command(backend, line): elif command == '/nick': # Change nick - # TODO: Validate nick + validate_nick(rest) + own_nick = rest set_status_nick(backend, own_status, own_nick) @@ -217,18 +267,30 @@ def handle_user_command(backend, line): # Display usage print('--- / ; /msg ; /status []; /available; /unavailable; /nick ; /target ; /quit') - except NoMatchesError as err: - print('--- name %s matches no peers' % err.args[0]) - - except TooManyMatchesError as err: - print('--- name %s matches several peers' % err.args[0]) - - else: - # Send message - if default_target_mac is None: - print('--- Default target not set, set with /target') else: - queue_message(backend, default_target_mac, line) + # Send message + if default_target_mac is None: + print('--- Default target not set, set with /target') + else: + queue_message(backend, default_target_mac, line) + + except NoMatchesError as err: + print('--- Name %s matches no peers' % err.args[0]) + + except TooManyMatchesError as err: + print('--- Name %s matches several peers' % err.args[0]) + + except NonCharacterError as err: + print('--- Error: contains non-character U+%04X' % ord(err.args[0])) + + except ControlCharacterError as err: + print('--- Error: contains control character U+%04X' % ord(err.args[0])) + + except NickLengthError: + print('--- Error: nick too long (max. 255B)') + + except MessageLengthError: + print('--- Error: message too long (max. 1494B)') def handle_status(mac, status, nick): global peers @@ -413,7 +475,7 @@ def eventloop(proc): raise ValueError('Unknown event type from backend: %s' % repr(event_type)) elif fd == proc.stdout.fileno() and event & select.POLLHUP: - print('Backend exited') + print('--- Backend exited') running = False elif fd == unbuf_stdin.fileno() and event & select.POLLIN: @@ -426,7 +488,12 @@ def eventloop(proc): break line, _, input_buffer = input_buffer.partition(b'\n') - if handle_user_command(proc.stdin, line.decode('utf-8')) == 'quit': + try: + line = line.decode('utf-8') + except UnicodeDecodeError: + print('--- Error: malformed utf-8') + + if handle_user_command(proc.stdin, line) == 'quit': writeall(proc.stdin, b'q') running = False