CVE-2015-10062 (published 2023-01-17T19:15Z)

A vulnerability, which was classified as problematic, was found in galaxy-data-resource up to 14.10.0. This affects an unknown part of the component Command Line Template. The manipulation leads to injection. Upgrading to version 14.10.1 is able to address this issue. The patch is named 50d65f45d3f5be5d1fbff2e45ac5cec075f07d42. It is recommended to upgrade the affected component. The associated identifier of this vulnerability is VDB-218451.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/blankenberg/galaxy-data-resource
commit_details: repo_url: https://github.com/blankenberg/galaxy-data-resource
author: Daniel Blankenberg
author_date: 2015-01-13 10:27:49-05:00
msg: Fix a critical security vulnerability where unsanitized user-modifiable values could be included in a command line template.
repository: repo_url: https://github.com/blankenberg/galaxy-data-resource
repo_name: blankenberg/galaxy-data-resource
repo_language: Python
owner: blankenberg
file_changes: filename: metadata.py
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,7 @@ from sqlalchemy.orm import object_session
 
 import galaxy.model
 from galaxy.util import listify
+from galaxy.util.object_wrapper import sanitize_lists_to_string
 from galaxy.util import stringify_dictionary_keys
 from galaxy.util import string_as_bool
 from galaxy.util import in_directory
@@ -232,6 +233,9 @@ class MetadataParameter( object ):
     def to_string( self, value ):
         return str( value )
 
+    def to_safe_string( self, value ):
+        return sanitize_lists_to_string( self.to_string( value ) )
+
     def make_copy( self, value, target_context = None, source_context = None ):
         return copy.deepcopy( value )
 
@@ -480,6 +484,10 @@ class DictParameter( MetadataParameter ):
     def to_string( self, value ):
         return  json.dumps( value )
 
+    def to_safe_string( self, value ):
+        # We do not sanitize json dicts
+        return json.safe_dumps( value )
+
 
 class PythonObjectParameter( MetadataParameter ):
 
@@ -510,6 +518,10 @@ class FileParameter( MetadataParameter ):
             return str( self.spec.no_value )
         return value.file_name
 
+    def to_safe_string( self, value ):
+        # We do not sanitize file names
+        return self.to_string( value )
+
     def get_html_field( self, value=None, context=None, other_values=None, **kwd ):
         context = context or {}
         other_values = other_values or {}

programming_language: Python
filename: evaluation.py
change_type: ModificationType.MODIFY
diff: @@ -2,10 +2,12 @@ import os
 import tempfile
 
 from galaxy import model
+from galaxy.util.object_wrapper import wrap_with_safe_string
 from galaxy.util.bunch import Bunch
 from galaxy.util.none_like import NoneDataset
 from galaxy.util.template import fill_template
 from galaxy.tools.wrappers import (
+    ToolParameterValueWrapper,
     DatasetFilenameWrapper,
     DatasetListWrapper,
     DatasetCollectionWrapper,
@@ -114,6 +116,9 @@ class ToolEvaluator( object ):
         self.__populate_input_dataset_wrappers(param_dict, input_datasets, input_dataset_paths)
         self.__populate_output_dataset_wrappers(param_dict, output_datasets, output_paths, job_working_directory)
         self.__populate_unstructured_path_rewrites(param_dict)
+        # Call param dict sanitizer, before non-job params are added, as we don't want to sanitize filenames.
+        self.__sanitize_param_dict( param_dict )
+        # Parameters added after this line are not sanitized
         self.__populate_non_job_params(param_dict)
 
         # Return the dictionary of parameters
@@ -334,6 +339,24 @@ class ToolEvaluator( object ):
             #the paths rewritten.
             self.__walk_inputs( self.tool.inputs, param_dict, rewrite_unstructured_paths )
 
+    def __sanitize_param_dict( self, param_dict ):
+        """
+        Sanitize all values that will be substituted on the command line, with the exception of ToolParameterValueWrappers,
+        which already have their own specific sanitization rules and also exclude special-cased named values.
+        We will only examine the first level for values to skip; the wrapping function will recurse as necessary.
+        
+        Note: this method follows the style of the similar populate calls, in that param_dict is modified in-place.
+        """
+        # chromInfo is a filename, do not sanitize it.
+        skip = [ 'chromInfo' ]
+        if not self.tool or not self.tool.options or self.tool.options.sanitize:
+            for key, value in param_dict.items():
+                if key not in skip:
+                    # Remove key so that new wrapped object will occupy key slot
+                    del param_dict[key]
+                    # And replace with new wrapped key
+                    param_dict[ wrap_with_safe_string( key, no_wrap_classes=ToolParameterValueWrapper ) ] = wrap_with_safe_string( value, no_wrap_classes=ToolParameterValueWrapper )
+
     def build( self ):
         """
         Build runtime description of job to execute, evaluate command and

programming_language: Python
filename: wrappers.py
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@ import pipes
 from galaxy import exceptions
 from galaxy.util.none_like import NoneDataset
 from galaxy.util import odict
+from galaxy.util.object_wrapper import wrap_with_safe_string
 
 from logging import getLogger
 log = getLogger( __name__ )
@@ -162,10 +163,13 @@ class DatasetFilenameWrapper( ToolParameterValueWrapper ):
             if name in self.metadata.spec:
                 if rval is None:
                     rval = self.metadata.spec[name].no_value
-                rval = self.metadata.spec[name].param.to_string( rval )
+                rval = self.metadata.spec[ name ].param.to_safe_string( rval )
                 # Store this value, so we don't need to recalculate if needed
                 # again
                 setattr( self, name, rval )
+            else:
+                #escape string value of non-defined metadata value
+                rval = wrap_with_safe_string( rval )
             return rval
 
         def __nonzero__( self ):
@@ -190,9 +194,13 @@ class DatasetFilenameWrapper( ToolParameterValueWrapper ):
                 ext = tool.inputs[name].extensions[0]
             except:
                 ext = 'data'
-            self.dataset = NoneDataset( datatypes_registry=datatypes_registry, ext=ext )
+            self.dataset = wrap_with_safe_string( NoneDataset( datatypes_registry=datatypes_registry, ext=ext ), no_wrap_classes=ToolParameterValueWrapper )
         else:
-            self.dataset = dataset
+            # Tool wrappers should not normally be accessing .dataset directly, 
+            # so we will wrap it and keep the original around for file paths
+            # Should we name this .value to maintain consistency with most other ToolParameterValueWrapper?
+            self.unsanitized = dataset
+            self.dataset = wrap_with_safe_string( dataset, no_wrap_classes=ToolParameterValueWrapper )
             self.metadata = self.MetadataWrapper( dataset.metadata )
         self.datatypes_registry = datatypes_registry
         self.false_path = getattr( dataset_path, "false_path", None )
@@ -210,7 +218,7 @@ class DatasetFilenameWrapper( ToolParameterValueWrapper ):
         if self.false_path is not None:
             return self.false_path
         else:
-            return self.dataset.file_name
+            return self.unsanitized.file_name
 
     def __getattr__( self, key ):
         if self.false_path is not None and key == 'file_name':
@@ -230,7 +238,7 @@ class DatasetFilenameWrapper( ToolParameterValueWrapper ):
                 # object store to find the static location of this
                 # directory.
                 try:
-                    return self.dataset.extra_files_path
+                    return self.unsanitized.extra_files_path
                 except exceptions.ObjectNotFound:
                     # NestedObjectstore raises an error here
                     # instead of just returning a non-existent

programming_language: Python
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -360,46 +360,57 @@ mapped_chars = { '>': '__gt__',
                  '#': '__pd__'}
 
 
-def restore_text(text):
+def restore_text( text, character_map=mapped_chars ):
     """Restores sanitized text"""
     if not text:
         return text
-    for key, value in mapped_chars.items():
+    for key, value in character_map.items():
         text = text.replace(value, key)
     return text
 
 
-def sanitize_text(text):
+def sanitize_text( text, valid_characters=valid_chars, character_map=mapped_chars, invalid_character='X' ):
     """
     Restricts the characters that are allowed in text; accepts both strings
-    and lists of strings.
+    and lists of strings; non-string entities will be cast to strings.
     """
-    if isinstance( text, basestring ):
-        return _sanitize_text_helper(text)
-    elif isinstance( text, list ):
-        return [ _sanitize_text_helper(t) for t in text ]
+    if isinstance( text, list ):
+        return map( lambda x: sanitize_text( x, valid_characters=valid_characters, character_map=character_map, invalid_character=invalid_character ), text )
+    if not isinstance( text, basestring ):
+        text = smart_str( text )
+    return _sanitize_text_helper( text, valid_characters=valid_characters, character_map=character_map )
 
-
-def _sanitize_text_helper(text):
+def _sanitize_text_helper( text, valid_characters=valid_chars, character_map=mapped_chars, invalid_character='X' ):
     """Restricts the characters that are allowed in a string"""
 
     out = []
     for c in text:
-        if c in valid_chars:
+        if c in valid_characters:
             out.append(c)
-        elif c in mapped_chars:
-            out.append(mapped_chars[c])
+        elif c in character_map:
+            out.append( character_map[c] )
         else:
-            out.append('X')  # makes debugging easier
+            out.append( invalid_character )  # makes debugging easier
     return ''.join(out)
 
 
-def sanitize_param(value):
+def sanitize_lists_to_string( values, valid_characters=valid_chars, character_map=mapped_chars, invalid_character='X'  ):
+    if isinstance( values, list ):
+        rval = []
+        for value in values:
+            rval.append( sanitize_lists_to_string( value, valid_characters=valid_characters, character_map=character_map, invalid_character=invalid_character ) )
+        values = ",".join( rval )
+    else:
+        values = sanitize_text( values, valid_characters=valid_characters, character_map=character_map, invalid_character=invalid_character )
+    return values
+
+
+def sanitize_param( value, valid_characters=valid_chars, character_map=mapped_chars, invalid_character='X' ):
     """Clean incoming parameters (strings or lists)"""
     if isinstance( value, basestring ):
-        return sanitize_text(value)
+        return sanitize_text( value, valid_characters=valid_characters, character_map=character_map, invalid_character=invalid_character )
     elif isinstance( value, list ):
-        return map(sanitize_text, value)
+        return map( lambda x: sanitize_text( x, valid_characters=valid_characters, character_map=character_map, invalid_character=invalid_character ), value )
     else:
         raise Exception('Unknown parameter type (%s)' % ( type( value ) ))
 

programming_language: Python
filename: dbkeys.py
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,7 @@ Functionality for dealing with dbkeys.
 #dbkeys read from disk using builds.txt
 from galaxy.util import read_dbnames
 from galaxy.util.json import loads
+from galaxy.util.object_wrapper import sanitize_lists_to_string
 import os.path
 
 
@@ -84,6 +85,7 @@ class GenomeBuilds( object ):
         # use configured server len path
         if not chrom_info:
             # Default to built-in build.
-            chrom_info = os.path.join( self._static_chrom_info_path, "%s.len" % dbkey )
+            # Since we are using an unverified dbkey, we will sanitize the dbkey before use
+            chrom_info = os.path.join( self._static_chrom_info_path, "%s.len" % sanitize_lists_to_string( dbkey ) )
         chrom_info = os.path.abspath( chrom_info )
         return ( chrom_info, db_dataset )

programming_language: Python
filename: object_wrapper.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,436 @@
+"""
+Classes for wrapping Objects and Sanitizing string output.
+"""
+
+import inspect
+import copy_reg
+import logging
+import string
+from numbers import Number
+from types import ( NoneType, NotImplementedType, EllipsisType, FunctionType, MethodType, GeneratorType, CodeType,
+                    BuiltinFunctionType, BuiltinMethodType, ModuleType, XRangeType, SliceType, TracebackType, FrameType,
+                    BufferType, DictProxyType, GetSetDescriptorType, MemberDescriptorType )
+from UserDict import UserDict
+
+from galaxy.util import sanitize_lists_to_string as _sanitize_lists_to_string
+
+log = logging.getLogger( __name__ )
+
+# Define different behaviors for different types, see also: https://docs.python.org/2/library/types.html
+
+# Known Callable types
+__CALLABLE_TYPES__ = ( FunctionType, MethodType, GeneratorType, CodeType, BuiltinFunctionType, BuiltinMethodType, )
+
+# Always wrap these types without attempting to subclass
+__WRAP_NO_SUBCLASS__ =  ( ModuleType, XRangeType, SliceType, BufferType, TracebackType, FrameType, DictProxyType,
+                          GetSetDescriptorType, MemberDescriptorType ) + __CALLABLE_TYPES__
+
+# Don't wrap or sanitize.
+__DONT_SANITIZE_TYPES__ = ( Number, bool, NoneType, NotImplementedType, EllipsisType, bytearray, )
+
+# Don't wrap, but do sanitize.
+__DONT_WRAP_TYPES__ = tuple() #( basestring, ) so that we can get the unsanitized string, we will now wrap basestring instances
+
+# Wrap contents, but not the container
+__WRAP_SEQUENCES__ = ( tuple, list, )
+__WRAP_SETS__ = ( set, frozenset, )
+__WRAP_MAPPINGS__ = ( dict, UserDict, )
+
+
+# Define the set of characters that are not sanitized, and define a set of mappings for those that are.
+# characters that are valid
+VALID_CHARACTERS = set( string.letters + string.digits + " -=_.()/+*^,:?!@" )
+
+# characters that are allowed but need to be escaped
+CHARACTER_MAP = { '>': '__gt__',
+                 '<': '__lt__',
+                 "'": '__sq__',
+                 '"': '__dq__',
+                 '[': '__ob__',
+                 ']': '__cb__',
+                 '{': '__oc__',
+                 '}': '__cc__',
+                 '\n': '__cn__',
+                 '\r': '__cr__',
+                 '\t': '__tc__',
+                 '#': '__pd__'}
+
+INVALID_CHARACTER = "X"
+
+def sanitize_lists_to_string( values, valid_characters=VALID_CHARACTERS, character_map=CHARACTER_MAP, invalid_character=INVALID_CHARACTER  ):
+    return _sanitize_lists_to_string( values, valid_characters=valid_characters, character_map=character_map, invalid_character=invalid_character  )
+
+
+def wrap_with_safe_string( value, no_wrap_classes = None ):
+    """
+    Recursively wrap values that should be wrapped.
+    """
+
+    def __do_wrap( value ):
+        if isinstance( value, SafeStringWrapper ):
+            # Only ever wrap one-layer
+            return value
+        if callable( value ):
+            safe_class = CallableSafeStringWrapper
+        else:
+            safe_class = SafeStringWrapper
+        if isinstance( value, no_wrap_classes ):
+            return value
+        if isinstance( value, __DONT_WRAP_TYPES__ ):
+            return sanitize_lists_to_string( value, valid_characters=VALID_CHARACTERS, character_map=CHARACTER_MAP )
+        if isinstance( value, __WRAP_NO_SUBCLASS__ ):
+            return safe_class( value, safe_string_wrapper_function = __do_wrap )
+        for this_type in __WRAP_SEQUENCES__ + __WRAP_SETS__:
+            if isinstance( value, this_type ):
+                return this_type( map( __do_wrap, value ) )
+        for this_type in __WRAP_MAPPINGS__:
+            if isinstance( value, this_type ):
+                # Wrap both key and value
+                return this_type( map( lambda x: ( __do_wrap( x[0] ), __do_wrap( x[1] ) ), value.items() ) )
+        # Create a dynamic class that joins SafeStringWrapper with the object being wrapped.
+        # This allows e.g. isinstance to continue to work.
+        try:
+            wrapped_class_name = value.__name__
+            wrapped_class = value
+        except:
+            wrapped_class_name = value.__class__.__name__
+            wrapped_class = value.__class__
+        value_mod = inspect.getmodule( value )
+        if value_mod:
+            wrapped_class_name = "%s.%s" % ( value_mod.__name__, wrapped_class_name )
+        wrapped_class_name = "SafeStringWrapper(%s:%s)" % ( wrapped_class_name, ",".join( sorted( map( str, no_wrap_classes ) ) ) )
+        do_wrap_func_name = "__do_wrap_%s" % ( wrapped_class_name )
+        do_wrap_func = __do_wrap
+        global_dict = globals()
+        if wrapped_class_name in global_dict:
+            # Check to see if we have created a wrapper for this class yet, if so, reuse
+            wrapped_class = global_dict.get( wrapped_class_name )
+            do_wrap_func = global_dict.get( do_wrap_func_name, __do_wrap )
+        else:
+            try:
+                wrapped_class = type( wrapped_class_name, ( safe_class, wrapped_class, ), {} )
+            except TypeError, e:
+                 # Fail-safe for when a class cannot be dynamically subclassed.
+                 log.warning( "Unable to create dynamic subclass for %s, %s: %s", type( value), value, e )
+                 wrapped_class = type( wrapped_class_name, ( safe_class, ), {} )
+            if wrapped_class not in ( SafeStringWrapper, CallableSafeStringWrapper ):
+                # Save this wrapper for reuse and pickling/copying
+                global_dict[ wrapped_class_name ] = wrapped_class
+                do_wrap_func.__name__ = do_wrap_func_name
+                global_dict[ do_wrap_func_name ] = do_wrap_func
+                def pickle_safe_object( safe_object ):
+                    return ( wrapped_class, ( safe_object.unsanitized, do_wrap_func, ) )
+                # Set pickle and copy properties
+                copy_reg.pickle( wrapped_class, pickle_safe_object, do_wrap_func )
+        return wrapped_class( value, safe_string_wrapper_function = do_wrap_func )
+    # Determine classes not to wrap
+    if no_wrap_classes:
+        if not isinstance( no_wrap_classes, ( tuple, list ) ):
+            no_wrap_classes = [ no_wrap_classes ]
+        no_wrap_classes = list( no_wrap_classes ) + list( __DONT_SANITIZE_TYPES__ ) + [ SafeStringWrapper ]
+    else:
+        no_wrap_classes = list( __DONT_SANITIZE_TYPES__ ) + [ SafeStringWrapper ]
+    no_wrap_classes = tuple( set( sorted( no_wrap_classes, key=str ) ) )
+    return __do_wrap( value )
+
+
+# N.B. refer to e.g. https://docs.python.org/2/reference/datamodel.html for information on Python's Data Model.
+
+
+class SafeStringWrapper( object ):
+    """
+    Class that wraps and sanitizes any provided value's attributes 
+    that will attempt to be cast into a string.
+    
+    Attempts to mimic behavior of original class, including operands.
+    
+    To ensure proper handling of e.g. subclass checks, the *wrap_with_safe_string()*
+    method should be used.
+    
+    This wrapping occurs in a recursive/parasitic fashion, as all called attributes of 
+    the originally wrapped object will also be wrapped and sanitized, unless the attribute
+    is of a type found in __DONT_SANITIZE_TYPES__ + __DONT_WRAP_TYPES__, where e.g. ~(strings
+    will still be sanitized, but not wrapped), and e.g. integers will have neither.
+    """
+    __UNSANITIZED_ATTRIBUTE_NAME__ = 'unsanitized' 
+    __NO_WRAP_NAMES__ = [ '__safe_string_wrapper_function__', __UNSANITIZED_ATTRIBUTE_NAME__]
+
+
+    def __new__( cls, *arg, **kwd ):
+        # We need to define a __new__ since, we are subclassing from e.g. immutable str, which internally sets data 
+        # that will be used when other + this (this + other is handled by __add__)
+        safe_string_wrapper_function = kwd.get( 'safe_string_wrapper_function', None) or wrap_with_safe_string
+        try:
+            return super( SafeStringWrapper, cls ).__new__( cls, sanitize_lists_to_string( arg[0], valid_characters=VALID_CHARACTERS, character_map=CHARACTER_MAP ) )
+        except Exception, e:
+            log.warning( "Could not provide an argument to %s.__new__: %s; will try without arguments.", cls, e )
+            return super( SafeStringWrapper, cls ).__new__( cls )
+
+    def __init__( self, value, safe_string_wrapper_function = wrap_with_safe_string ):
+        self.unsanitized = value
+        self.__safe_string_wrapper_function__ = safe_string_wrapper_function
+
+    def __str__( self ):
+        return sanitize_lists_to_string( self.unsanitized, valid_characters=VALID_CHARACTERS, character_map=CHARACTER_MAP )
+
+    def __repr__( self ):
+        return "%s object at %x on: %s" % ( sanitize_lists_to_string( self.__class__.__name__, valid_characters=VALID_CHARACTERS, character_map=CHARACTER_MAP ), id( self ), sanitize_lists_to_string( repr( self.unsanitized ), valid_characters=VALID_CHARACTERS, character_map=CHARACTER_MAP ) )
+
+    def __lt__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized < other
+
+    def __le__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized <= other
+
+    def __eq__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized == other
+
+    def __ne__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized != other
+
+    def __gt__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized > other
+
+    def __ge__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized >= other
+
+    def __lt__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.unsanitized < other
+
+    def __cmp__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return cmp( self.unsanitized, other )
+
+    # Do not implement __rcmp__, python 2.2 < 2.6
+
+    def __hash__( self ):
+        return hash( self.unsanitized )
+
+    def __nonzero__( self ):
+        return bool( self.unsanitized )
+
+    # Do not implement __unicode__, we will rely on __str__
+
+    def __getattr__( self, name ):
+        if name in SafeStringWrapper.__NO_WRAP_NAMES__:
+            #FIXME: is this ever reached?
+            return object.__getattr__( self, name )
+        return self.__safe_string_wrapper_function__( getattr( self.unsanitized, name ) )
+
+    def __setattr__( self, name, value ):
+        if name in SafeStringWrapper.__NO_WRAP_NAMES__:
+            return object.__setattr__( self, name, value )
+        return setattr( self.unsanitized, name, value )
+
+    def __delattr__( self, name ):
+        if name in SafeStringWrapper.__NO_WRAP_NAMES__:
+            return object.__delattr__( self, name )
+        return delattr( self.unsanitized, name )
+
+    def __getattribute__( self, name ):
+        if name in SafeStringWrapper.__NO_WRAP_NAMES__:
+            return object.__getattribute__( self, name )
+        return self.__safe_string_wrapper_function__( getattr( object.__getattribute__( self, 'unsanitized' ), name ) )
+
+    # Skip Descriptors
+
+    # Skip __slots__
+    
+    # Don't need __metaclass__, we'll use the helper function to handle with subclassing for e.g. isinstance()
+
+    # Revisit:
+    # __instancecheck__
+    # __subclasscheck__
+    # We are using a helper class to create dynamic subclasses to handle class checks
+
+    # We address __call__ as needed based upon unsanitized, through the use of a CallableSafeStringWrapper class
+
+    def __len__( self ):
+        original_value = self.unsanitized
+        while isinstance( original_value, SafeStringWrapper ):
+            original_value = self.unsanitized
+        return len( self.unsanitized )
+
+    def __getitem__( self, key ):
+        return self.__safe_string_wrapper_function__( self.unsanitized[ key ] )
+
+    def __setitem__( self, key, value ):
+        while isinstance( value, SafeStringWrapper ):
+            value = value.unsanitized
+        self.unsanitized[ key ] = value
+
+    def __delitem__( self, key ):
+        del self.unsanitized[ key ]
+
+    def __iter__( self ):
+        return iter( map( self.__safe_string_wrapper_function__, iter( self.unsanitized ) ) )
+
+    # Do not implement __reversed__
+
+    def __contains__( self, item ):
+        # FIXME: Do we need to consider if item is/isn't or does/doesn't contain SafeStringWrapper?
+        # When considering e.g. nested lists/dicts/etc, this gets complicated
+        while isinstance( item, SafeStringWrapper ):
+            item = item.unsanitized
+        return item in self.unsanitized
+
+    # Not sure that we need these slice methods, but will provide anyway
+    def __getslice__( self, i, j ):
+        return self.__safe_string_wrapper_function__( self.unsanitized[ i:j ] )
+
+    def __setslice__( self, i, j, value ):
+        self.unsanitized[ i:j ] = value
+
+    def __delslice__( self, i, j ):
+        del self.unsanitized[ i:j ]
+
+    def __add__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized + other )
+
+    def __sub__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized - other )
+
+    def __mul__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized * other )
+
+    def __floordiv__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized // other )
+
+    def __mod__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized % other )
+
+    def __divmod__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( divmod( self.unsanitized, other ) )
+
+    def __pow__( self, *other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( pow( self.unsanitized, *other ) )
+
+    def __lshift__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized << other )
+
+    def __rshift__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized >> other )
+
+    def __and__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized & other )
+
+    def __xor__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized ^ other )
+
+    def __or__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized | other )
+
+    def __div__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized / other )
+
+    def __truediv__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( self.unsanitized / other )
+
+    # The only reflected operand that we will define is __rpow__, due to coercion rules complications as per docs
+    def __rpow__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return self.__safe_string_wrapper_function__( pow( other, self.unsanitized ) )
+
+    # Do not implement in-place operands
+
+    def __neg__( self ):
+        return __safe_string_wrapper_function__( -self.unsanitized )
+
+    def __pos__( self ):
+        return __safe_string_wrapper_function__( +self.unsanitized )
+
+    def __abs__( self ):
+        return __safe_string_wrapper_function__( abs( self.unsanitized ) )
+
+    def __invert__( self ):
+        return __safe_string_wrapper_function__( ~self.unsanitized )
+
+    def __complex__( self ):
+        return __safe_string_wrapper_function__( complex( self.unsanitized ) )
+
+    def __int__( self ):
+        return int( self.unsanitized )
+
+    def __float__( self ):
+        return float( self.unsanitized )
+
+    def __oct__( self ):
+        return oct( self.unsanitized )
+
+    def __hex__( self ):
+        return hex( self.unsanitized )
+
+    def __index__( self ):
+        return self.unsanitized.index()
+
+    def __coerce__( self, other ):
+        while isinstance( other, SafeStringWrapper ):
+            other = other.unsanitized
+        return coerce( self.unsanitized, other )
+
+    def __enter__( self ):
+        return self.unsanitized.__enter__()
+
+    def __exit__( self, *args ):
+        return self.unsanitized.__exit__( *args )
+
+class CallableSafeStringWrapper( SafeStringWrapper ):
+    
+    def __call__( self, *args, **kwds ):
+        return self.__safe_string_wrapper_function__( self.unsanitized( *args, **kwds ) )
+
+
+# Enable pickling/deepcopy
+def pickle_SafeStringWrapper( safe_object ):
+    args = ( safe_object.unsanitized, )
+    cls = SafeStringWrapper
+    if isinstance( safe_object, CallableSafeStringWrapper ):
+        cls = CallableSafeStringWrapper
+    return ( cls, args )
+copy_reg.pickle( SafeStringWrapper, pickle_SafeStringWrapper, wrap_with_safe_string )
+copy_reg.pickle( CallableSafeStringWrapper, pickle_SafeStringWrapper, wrap_with_safe_string )
+

programming_language: Python