diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bba0033a..6658661b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,6 +186,9 @@ without arguments starts a full interactive Python console. - Fixes in multi-match situations - don't allow finding/listing multimatches for 3-box when only two boxes in location. - Fix for TaskHandler with proper deferred returns/ability to cancel etc (PR by davewiththenicehat) +- Add `PermissionHandler.check` method for straight string perm-checks without needing lockstrings. +- Add `evennia.utils.utils.strip_unsafe_input` for removing html/newlines/tags from user input. The + `INPUT_CLEANUP_BYPASS_PERMISSIONS` is a list of perms that bypass this safety stripping. ## Evennia 0.9 (2018-2019) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index fe56df9334..c7c95cac62 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -15,7 +15,7 @@ from evennia.locks.lockhandler import LockException from evennia.comms.comms import DefaultChannel from evennia.utils import create, logger, utils from evennia.utils.logger import tail_log_file -from evennia.utils.utils import class_from_module +from evennia.utils.utils import class_from_module, strip_unsafe_input from evennia.utils.evmenu import ask_yes_no COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -298,6 +298,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): caller.msg(f"You are not allowed to send messages to channel {channel}") return + # avoid unsafe tokens in message + message = strip_unsafe_input(message, self.session) + channel.msg(message, senders=self.caller, **kwargs) def get_channel_history(self, channel, start_index=0): diff --git a/evennia/server/inputfuncs.py b/evennia/server/inputfuncs.py index 700881e99a..adfbc18216 100644 --- a/evennia/server/inputfuncs.py +++ b/evennia/server/inputfuncs.py @@ -59,6 +59,7 @@ def text(session, *args, **kwargs): arguments are ignored. """ + # from evennia.server.profiling.timetrace import timetrace # text = timetrace(text, "ServerSession.data_in") diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 3c37b3f2e0..f22ba2737a 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -722,6 +722,12 @@ CREATION_THROTTLE_LIMIT = 2 CREATION_THROTTLE_TIMEOUT = 10 * 60 LOGIN_THROTTLE_LIMIT = 5 LOGIN_THROTTLE_TIMEOUT = 5 * 60 +# Certain characters, like html tags, line breaks and tabs are stripped +# from user input for commands using the `evennia.utils.strip_unsafe_input` helper +# since they can be exploitative. This list defines Account-level permissions +# (and higher) that bypass this stripping. It is used as a fallback if a +# specific list of perms are not given to the helper function. +INPUT_CLEANUP_BYPASS_PERMISSIONS = ['Builder'] ###################################################################### diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 605a133397..7fc0b34da0 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -252,6 +252,9 @@ class ANSIParser(object): # instance of each ansi_escapes = re.compile(r"(%s)" % "|".join(ANSI_ESCAPES), re.DOTALL) + # tabs/linebreaks |/ and |- should be able to be cleaned + unsafe_tokens = re.compile(r"\|\/|\|-", re.DOTALL) + def sub_ansi(self, ansimatch): """ Replacer used by `re.sub` to replace ANSI @@ -430,6 +433,13 @@ class ANSIParser(object): string = self.mxp_url_sub.sub(r"\1", string) # replace with url verbatim return string + def strip_unsafe_tokens(self, string): + """ + Strip explicitly ansi line breaks and tabs. + + """ + return self.unsafe_tokens.sub('', string) + def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False): """ Parses a string, subbing color codes according to the stored @@ -564,6 +574,15 @@ def strip_raw_ansi(string, parser=ANSI_PARSER): return parser.strip_raw_codes(string) +def strip_unsafe_tokens(string, parser=ANSI_PARSER): + """ + Strip markup that can be used to create visual exploits + (notably linebreaks and tags) + + """ + return parser.strip_unsafe_tokens(string) + + def raw(string): """ Escapes a string into a form which won't be colorized by the ansi diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 8c29fef433..53d410098d 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -24,12 +24,13 @@ from simpleeval import simple_eval from unicodedata import east_asian_width from twisted.internet.task import deferLater from twisted.internet.defer import returnValue # noqa - used as import target +from twisted.internet import threads, reactor from os.path import join as osjoin from inspect import ismodule, trace, getmembers, getmodule, getmro from collections import defaultdict, OrderedDict -from twisted.internet import threads, reactor from django.conf import settings from django.utils import timezone +from django.utils.html import strip_tags from django.utils.translation import gettext as _ from django.apps import apps from django.core.validators import validate_email as django_validate_email @@ -44,6 +45,7 @@ ENCODINGS = settings.ENCODINGS _TASK_HANDLER = None _TICKER_HANDLER = None +_STRIP_UNSAFE_TOKENS = None _GA = object.__getattribute__ _SA = object.__setattr__ @@ -2588,3 +2590,41 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs): if raise_errors: raise return args, kwargs + + +def strip_unsafe_input(txt, session=None, bypass_perms=None): + """ + Remove 'unsafe' text codes from text; these are used to elimitate + exploits in user-provided data, such as html-tags, line breaks etc. + + Args: + txt (str): The text to clean. + session (Session, optional): A Session in order to determine if + the check should be bypassed by permission (will be checked + with the 'perm' lock, taking permission hierarchies into account). + bypass_perms (list, optional): Iterable of permission strings + to check for bypassing the strip. If not given, use + `settings.INPUT_CLEANUP_BYPASS_PERMISSIONS`. + + Returns: + str: The cleaned string. + + Notes: + The `INPUT_CLEANUP_BYPASS_PERMISSIONS` list defines what account + permissions are required to bypass this strip. + + """ + global _STRIP_UNSAFE_TOKENS + if not _STRIP_UNSAFE_TOKENS: + from evennia.utils.ansi import strip_unsafe_tokens as _STRIP_UNSAFE_TOKENS + + if session: + obj = session.puppet if session.puppet else session.account + bypass_perms = bypass_perms or settings.INPUT_CLEANUP_BYPASS_PERMISSIONS + if obj.permissions.check(*bypass_perms): + return txt + + # remove html codes + txt = strip_tags(txt) + txt = _STRIP_UNSAFE_TOKENS(txt) + return txt