# -*- coding: utf-8 -*- ''' Taint Mode for Python via a Library Copyright 2009 Juan José Conti Copyright 2010 Juan José Conti - Alejandro Russo This file is part of taintmode.py taintmode is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. taintmode.py is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with taintmode.py. If not, see . ''' import inspect import sys from itertools import chain __version__ = 'trunk-svn-2' __all__ = ['tainted', 'taint', 'untrusted', 'untrusted_args', 'ssink', 'validator', 'cleaner', 'STR', 'INT', 'FLOAT', 'UNICODE', 'chr', 'ord', 'len', 'ends_execution', 'XSS', 'SQLI', 'OSI', 'II'] ENDS = False RAISES = False KEYS = [XSS, SQLI, OSI, II] = range(1, 5) TAGS = set(KEYS) class TaintException(Exception): pass def ends_execution(b=True): global ENDS ENDS = b # ------------------------- Taint-aware functions ----------------------------- def propagate_func(original): def inner (*args, **kwargs): t = set() for a in args: collect_tags(a, t) for v in kwargs.values(): collect_tags(v, t) r = original(*args, **kwargs) if t: r = taint_aware(r, t) return r return inner len = propagate_func(len) ord = propagate_func(ord) chr = propagate_func(chr) # ------------------------- Auxiliaries functions ----------------------------- def mapt(o, f, check=lambda o: type(o) in tclasses): if check(o): return f(o) elif isinstance(o, list): return [mapt(x, f, check) for x in o] elif isinstance(o, tuple): return tuple(mapt(x, f, check) for x in o) elif isinstance(o, set): return set(mapt(x, f, check) for x in o) elif isinstance(o, dict): klass = type(o) # It's quite common for frameworks to extend dict # with useful new methdos - i.e. web.py return klass((k, mapt(v, f, check)) for k, v in o.iteritems()) else: return o def remove_taint(v): def _remove(o): if hasattr(o, 'taints'): o.taints.discard(v) return _remove def remove_tags(r, v): mapt(r, remove_taint(v), lambda o: True) def collect_tags(s, t): '''Collect tags from a source s into a target t.''' mapt(s, lambda o: t.update(o.taints), lambda o: hasattr(o, 'taints')) def update_tags(r, t): mapt(r, lambda o: o.taints.update(t), lambda o: hasattr(o, 'taints')) def taint_aware(r, ts=set()): r = mapt(r, tclass) update_tags(r, ts) return r # ------------------------- Decorators ---------------------------------------- def untrusted_args(nargs=[], nkwargs=[]): ''' Mark a function or method that would recive untrusted values. nargs is a list of positions. Positional arguments in that position will be tainted for all the types of taint. nkwargs is a list of strings. Keyword arguments for those keys will be tainted for all the types of taint. Some frameworks works as follow: they ask the programmer to write certain function or method in such a way that then the framework itself use it to pass to the program values received from the outside. Twisted, the framework for building network applications, gives us an accurate example when you extend the LineOnlyReceiver class: .. code-block:: python class MyProtocol(LineOnlyReceiver): def lineReceived(self, line): self.doSomething(line) # line is a tainted value It's complicated or even impossible use the untrusted decorator in this kind of situations. Instead of it, you should use the untrusted_args decorator, which receives as an optional argument a list of non-trusted arguments positions and a list of keywords. The line parameter in the example can be marked as untrusted in this way: .. code-block:: python class MyProtocol(LineOnlyReceiver): @untrusted_args([1]) def lineReceived(self, line): self.doSomething(line) Example ======= >>> import taintmode >>> from taintmode import * >>> @ssink(OSI) ... def exec_comand(cm): ... print "executing ", cm ... >>> @untrusted_args([1]) ... def rutine(auxvalue, command): ... exec_comand(command) ... print auxvalue ... >>> rutine(42,"rm -r /") =============================================================================== Violation in line 3 from file Tainted value: rm -r / ------------------------------------------------------------------------------- print auxvalue =============================================================================== executing rm -r / 42 ''' def _untrusted_args(f): def inner(*args, **kwargs): args = list(args) # args is a tuple - add a test for n in nargs: args[n] = mapt(args[n], taint) for n in nkwargs: kwargs[n] = mapt(kwargs[n], taint) r = f(*args, **kwargs) return r return inner return _untrusted_args def untrusted(f): ''' Mark a function or method as untrusted. The returned value will be tainted for all the types of taint. Examples ======== If you have access to the function or method definition, for example if it's part of your code-base the decorator can be applied using Python's syntactic sugar: >>> @untrusted ... def from_outside(): ... return 'a string' #this value is untrusted ... >>> value = from_outside() >>> value 'a string' >>> type(value) >>> tainted(value) True While using third-party modules, we still can apply the decorator. The next example is from a program writed using the web.py framework: >>> import web >>> web.input = untrusted(web.input) ''' def inner(*args, **kwargs): r = f(*args, **kwargs) return taint_aware(r, TAGS) return inner def validator(v, cond=True, nargs=[], nkwargs=[]): ''' Mark a function or method as capable to validate its input. nargs is a list of positions. Positional arguments in that positions are the ones validated. nkwargs is a list of strings. Keyword arguments for those keys are the ones validated. If the function returns cond, v will be removed from the the validated inpunt. Example: for a function called invalid_mail, cond is liked to be False. If invalid_mail returns False, then the mail IS valid and have no craft data on it. for a function called valid_mail, cond is liked to be True. ''' def _validator(f): def inner(*args, **kwargs): r = f(*args, **kwargs) if r == cond: tovalid = set(args[n] for n in nargs) tovalid.update(kwargs[n] for n in nkwargs) for a in tovalid: remove_tags(a, v) return r return inner return _validator def cleaner(v): ''' Mark a function or methos as capable to clean its input. :param v: taint removed from the returned value. Example ======= >>> @cleaner(SQLI) #this make the following function capable to clean objets from SQLI taints ... def clean_string(stri): ... return stri.split(' ')[0] ... >>> @untrusted #simulates an utrusted value ... def get_id_employee(): ... return "21 or 1=1" ... >>> @ssink(SQLI) #simulate a rutine that will execute an sql query ... def erase_employee(id): ... print "this value this value is placed in the WHERE clause of a sql delete statment:" , id >>> i=get_id_employee() >>> erase_employee(i) =============================================================================== Violation in line 1 from file Tainted value: 21 or 1=1 ------------------------------------------------------------------------------- --> erase_employee(i) =============================================================================== this value this value is placed in the WHERE clause of a sql delete statment: 21 or 1=1 >>> i = clean_string(i) # the untrusted value is clean now >>> i '21' >>> erase_employee(i) this value this value is placed in the WHERE clause of a sql delete statment: 21 ''' def _cleaner(f): def inner(*args, **kwargs): r = f(*args, **kwargs) remove_tags(r, v) return r return inner return _cleaner def reached(t, v=None): ''' Execute if a tainted value reaches a sensitive sink for the vulnerability v. If the module-level variable ENDS is set to True, then the sink is not executed and the reached function is executed instead. If ENDS is set to False, the reached function is executed but the program continues its flow. The provided de facto implementation alerts that the violation happened and information to find the error. ''' frame = sys._getframe(3) filename = inspect.getfile(frame) lno = frame.f_lineno print "=" * 79 print "Violation in line %d from file %s" % (lno, filename) # Localize this message print "Tainted value: %s" % t print '-' * 79 lineas = inspect.findsource(frame)[0] lineas = [' %s' % l for l in lineas] lno = lno - 1 lineas[lno] = '--> ' + lineas[lno][4:] lineas = lineas[lno - 3: lno + 3] print "".join(lineas) print "=" * 79 def ssink(v=None, reached=reached): ''' Mark a function or method as sensitive to tainted data. If it is called with a value with the v tag (or any tag if v is None), it's not executed and reached is executed instead. These sinks are sensitive to a kind of vulnerability, and must be specified when the decorator is used Examples ======== The web.py framework offers SQL Injection sensitive sink examples: >>> import web >>> db = web.database(dbn="sqlite", db="DB_NAME") >>> db.delete=ssink(SQLI)(db.delete) >>> db.select = ssink(SQLI)(db.select) >>> db.insert = ssink(SQLI)(db.insert) Like the rest of decorators, if the sensitive sink is defined in our code, we can use syntactic sugar: @ssink(OSI): def list_dir(input) ... The decorator can also be used without specifying a vulnerability. In this case, the sink is marked as sensitive to every kind of vulnerability, although this is not a very common use case: @ssink(): def very_sensitive(input): ... ''' def _solve(a, f, args, kwargs): if ENDS: if RAISES: reached(a) raise TaintException() else: return reached(a) else: reached(a) return f(*args, **kwargs) def _ssink(f): def inner(*args, **kwargs): allargs = chain(args, kwargs.itervalues()) if v is None: # sensitive to ALL for a in allargs: t = set() collect_tags(a, t) if t: return _solve(a, f, args, kwargs) else: for a in allargs: t = set() collect_tags(a, t) if v in t: return _solve(a, f, args, kwargs) return f(*args, **kwargs) return inner return _ssink def tainted(o, v=None): ''' Tells if a value o, a tclass instance, is tainted for the given vulnerability v. If v is not provided, checks for all taints. If the value is tainted with at least one vulnerability, returns True. Example ======= >>> t1 = taint(42, OSI) >>> t2 = taint(42) >>> tainted(t1) True >>> tainted(t1, OSI) True >>> tainted(t1, II) False >>> tainted(t2, II) True >>> tainted(t2, SQLI) True >>> tainted(t2) True ''' if not hasattr(o, 'taints'): return False if v is not None: return v in o.taints return bool(o.taints) def taint(o, v=None): ''' Helper function for taint the value o with the vulnerability v. If v is not provided, taint with all types of taints. Example: tainting with all vulnerabilities ========================================== >>> t = taint(42) >>> t.taints set([1, 2, 3, 4]) >>> tainted(t, XSS) True >>> tainted(t, OSI) True >>> tainted(t, SQLI) True >>> tainted(t, II) True Example: tainting with Interpreter Injection vulnerability ========================================================== >>> taint(42, II) 42 >>> t = taint(42, II) >>> t.taints set([4]) >>> tainted(t, II) True >>> tainted(t, OSI) False ''' ts = set() if v is not None: ts.add(v) else: ts.update(TAGS) return taint_aware(o, ts) # ------------------------- Taint-aware classes ------------------------------- def propagate_method(method): def inner(self, *args, **kwargs): r = method(self, *args, **kwargs) t = set() for a in args: collect_tags(a, t) for v in kwargs.values(): collect_tags(v, t) t.update(self.taints) return taint_aware(r, t) return inner def taint_class(klass, methods=None): if not methods: methods = attributes(klass) class tklass(klass): def __new__(cls, *args, **kwargs): self = super(tklass, cls).__new__(cls, *args, **kwargs) #justificar analizar pq no init self.taints = set() # if any of the arguments is tainted, taint the object aswell for a in args: # this CHUNK of code appears at least 3 times, refactor later collect_tags(a, self.taints) for v in kwargs.values(): collect_tags(v, self.taints) return self # support for assigment and taint change in classobj def __setattr__(self, name, value): if self.__dict__ and name in self.__dict__ and tainted(self.__dict__[name]): for t in self.__dict__[name].taints: # if other field had it, keep it taintsets = [v.taints for k,v in self.__dict__.items() if not callable(v) and tainted(v) and k != name] if not any([t in x for x in taintsets]): self.taints.remove(t) if self.__dict__ is not None: self.__dict__[name] = value if tainted(value): self.taints.update(value.taints) d = klass.__dict__ for name, attr in [(m, d[m]) for m in methods]: if inspect.ismethod(attr) or inspect.ismethoddescriptor(attr): setattr(tklass, name, propagate_method(attr)) # str has no __radd__ method if '__add__' in methods and '__radd__' not in methods: setattr(tklass, '__radd__', lambda self, other: tklass.__add__(tklass(other), self)) # unicode __rmod__ returns NotImplemented if klass == unicode: setattr(tklass, '__rmod__', lambda self, other: tklass.__mod__(tklass(other), self)) return tklass dont_override = set(['__repr__', '__cmp__', '__getattribute__', '__new__', '__init__','__nonzero__', '__reduce__', '__reduce_ex__', '__str__', '__int__', '__float__', '__unicode__']) # ------- Taint-aware classes for strings, integers, floats, and unicode ------ def attributes(klass): a = set(klass.__dict__.keys()) return a - dont_override str_methods = attributes(str) unicode_methods = attributes(unicode) int_methods = attributes(int) float_methods = attributes(float) STR = taint_class(str, str_methods) UNICODE = taint_class(unicode, unicode_methods) INT = taint_class(int, int_methods) FLOAT = taint_class(float, float_methods) tclasses = {str: STR, int: INT, float: FLOAT, unicode: UNICODE} def tclass(o): '''Tainted instance factory.''' klass = type(o) if klass in tclasses.keys(): return tclasses[klass](o) else: raise KeyError if __name__ == "__main__": import doctest doctest.testmod()