Skip to content

Commit 56b6086

Browse files
fix(iast): forbbidenfruit package conflict (backport #4546) (#4549)
Remove forbiddenfruit as dependency and rollback wrapt changes where forbiddenfruit was called. IAST: Patch builtins only when IAST is enabled. Co-authored-by: Alberto Vara <[email protected]>
1 parent 619db15 commit 56b6086

File tree

6 files changed

+107
-35
lines changed

6 files changed

+107
-35
lines changed

ddtrace/appsec/iast/_patch.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import ctypes
2+
import gc
3+
4+
from ddtrace.internal.logger import get_logger
5+
from ddtrace.vendor.wrapt import FunctionWrapper
6+
from ddtrace.vendor.wrapt import resolve_path
7+
8+
9+
log = get_logger(__name__)
10+
11+
12+
def try_wrap_function_wrapper(module, name, wrapper):
13+
try:
14+
wrap_object(module, name, FunctionWrapper, (wrapper,))
15+
except (ImportError, AttributeError):
16+
log.debug("IAST patching. Module %s.%s not exists", module, name)
17+
18+
19+
def apply_patch(parent, attribute, replacement):
20+
try:
21+
setattr(parent, attribute, replacement)
22+
except (TypeError, AttributeError):
23+
patch_builtins(parent, attribute, replacement)
24+
25+
26+
def wrap_object(module, name, factory, args=(), kwargs={}):
27+
(parent, attribute, original) = resolve_path(module, name)
28+
wrapper = factory(original, *args, **kwargs)
29+
apply_patch(parent, attribute, wrapper)
30+
return wrapper
31+
32+
33+
def patchable_builtin(klass):
34+
refs = gc.get_referents(klass.__dict__)
35+
assert len(refs) == 1
36+
return refs[0]
37+
38+
39+
def patch_builtins(klass, attr, value):
40+
"""Based on forbiddenfruit package:
41+
https://github.com/clarete/forbiddenfruit/blob/master/forbiddenfruit/__init__.py#L421
42+
---
43+
Patch a built-in `klass` with `attr` set to `value`
44+
45+
This function monkey-patches the built-in python object `attr` adding a new
46+
attribute to it. You can add any kind of argument to the `class`.
47+
48+
It's possible to attach methods as class methods, just do the following:
49+
50+
>>> def myclassmethod(cls):
51+
... return cls(1.5)
52+
>>> curse(float, "myclassmethod", classmethod(myclassmethod))
53+
>>> float.myclassmethod()
54+
1.5
55+
56+
Methods will be automatically bound, so don't forget to add a self
57+
parameter to them, like this:
58+
59+
>>> def hello(self):
60+
... return self * 2
61+
>>> curse(str, "hello", hello)
62+
>>> "yo".hello()
63+
"yoyo"
64+
"""
65+
dikt = patchable_builtin(klass)
66+
67+
old_value = dikt.get(attr, None)
68+
old_name = "_c_%s" % attr # do not use .format here, it breaks py2.{5,6}
69+
70+
# Patch the thing
71+
dikt[attr] = value
72+
73+
if old_value:
74+
dikt[old_name] = old_value
75+
76+
try:
77+
dikt[attr].__name__ = old_value.__name__
78+
except (AttributeError, TypeError): # py2.5 will raise `TypeError`
79+
pass
80+
try:
81+
dikt[attr].__qualname__ = old_value.__qualname__
82+
except AttributeError:
83+
pass
84+
85+
ctypes.pythonapi.PyType_Modified(ctypes.py_object(klass))

ddtrace/appsec/iast/taint_sinks/_base.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from ddtrace.constants import IAST_CONTEXT_KEY
1212
from ddtrace.internal import _context
1313
from ddtrace.internal.logger import get_logger
14-
from ddtrace.vendor.wrapt import wrap_function_wrapper
1514

1615

1716
if TYPE_CHECKING: # pragma: no cover
@@ -77,10 +76,3 @@ def report(cls, evidence_value=""):
7776
}
7877
)
7978
_context.set_item(IAST_CONTEXT_KEY, report, span=span)
80-
81-
82-
def _wrap_function_wrapper_exception(module, name, wrapper):
83-
try:
84-
wrap_function_wrapper(module, name, wrapper)
85-
except (ImportError, AttributeError):
86-
log.debug("IAST patching. Module %s.%s not exists", module, name)

ddtrace/appsec/iast/taint_sinks/weak_hash.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from typing import TYPE_CHECKING
44

55
from ddtrace.appsec.iast import oce
6+
from ddtrace.appsec.iast._patch import try_wrap_function_wrapper
67
from ddtrace.appsec.iast.constants import EVIDENCE_ALGORITHM_TYPE
78
from ddtrace.appsec.iast.constants import VULN_INSECURE_HASHING_TYPE
89
from ddtrace.appsec.iast.taint_sinks._base import VulnerabilityBase
9-
from ddtrace.appsec.iast.taint_sinks._base import _wrap_function_wrapper_exception
1010
from ddtrace.internal.logger import get_logger
1111

1212

@@ -37,22 +37,22 @@ def patch():
3737
setattr(hashlib, "_datadog_patch", True)
3838

3939
if sys.version_info >= (3, 0, 0):
40-
_wrap_function_wrapper_exception("_hashlib", "HASH.digest", wrapped_digest_function)
41-
_wrap_function_wrapper_exception("_hashlib", "HASH.hexdigest", wrapped_digest_function)
42-
_wrap_function_wrapper_exception(("_%s" % MD5_DEF), "MD5Type.digest", wrapped_md5_function)
43-
_wrap_function_wrapper_exception(("_%s" % MD5_DEF), "MD5Type.hexdigest", wrapped_md5_function)
44-
_wrap_function_wrapper_exception(("_%s" % SHA1_DEF), "SHA1Type.digest", wrapped_sha1_function)
45-
_wrap_function_wrapper_exception(("_%s" % SHA1_DEF), "SHA1Type.hexdigest", wrapped_sha1_function)
40+
try_wrap_function_wrapper("_hashlib", "HASH.digest", wrapped_digest_function)
41+
try_wrap_function_wrapper("_hashlib", "HASH.hexdigest", wrapped_digest_function)
42+
try_wrap_function_wrapper(("_%s" % MD5_DEF), "MD5Type.digest", wrapped_md5_function)
43+
try_wrap_function_wrapper(("_%s" % MD5_DEF), "MD5Type.hexdigest", wrapped_md5_function)
44+
try_wrap_function_wrapper(("_%s" % SHA1_DEF), "SHA1Type.digest", wrapped_sha1_function)
45+
try_wrap_function_wrapper(("_%s" % SHA1_DEF), "SHA1Type.hexdigest", wrapped_sha1_function)
4646
else:
47-
_wrap_function_wrapper_exception("hashlib", MD5_DEF, wrapped_md5_function)
48-
_wrap_function_wrapper_exception("hashlib", SHA1_DEF, wrapped_sha1_function)
49-
_wrap_function_wrapper_exception("hashlib", "new", wrapped_new_function)
47+
try_wrap_function_wrapper("hashlib", MD5_DEF, wrapped_md5_function)
48+
try_wrap_function_wrapper("hashlib", SHA1_DEF, wrapped_sha1_function)
49+
try_wrap_function_wrapper("hashlib", "new", wrapped_new_function)
5050

5151
# pycryptodome methods
52-
_wrap_function_wrapper_exception("Crypto.Hash.MD5", "MD5Hash.digest", wrapped_md5_function)
53-
_wrap_function_wrapper_exception("Crypto.Hash.MD5", "MD5Hash.hexdigest", wrapped_md5_function)
54-
_wrap_function_wrapper_exception("Crypto.Hash.SHA1", "SHA1Hash.digest", wrapped_sha1_function)
55-
_wrap_function_wrapper_exception("Crypto.Hash.SHA1", "SHA1Hash.hexdigest", wrapped_sha1_function)
52+
try_wrap_function_wrapper("Crypto.Hash.MD5", "MD5Hash.digest", wrapped_md5_function)
53+
try_wrap_function_wrapper("Crypto.Hash.MD5", "MD5Hash.hexdigest", wrapped_md5_function)
54+
try_wrap_function_wrapper("Crypto.Hash.SHA1", "SHA1Hash.digest", wrapped_sha1_function)
55+
try_wrap_function_wrapper("Crypto.Hash.SHA1", "SHA1Hash.hexdigest", wrapped_sha1_function)
5656

5757

5858
@WeakHash.wrap

ddtrace/vendor/wrapt/wrappers.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -802,10 +802,8 @@ def lookup_attribute(parent, attribute):
802802
return vars(cls)[attribute]
803803
else:
804804
return getattr(parent, attribute)
805-
elif hasattr(parent, attribute):
806-
return getattr(parent, attribute)
807805
else:
808-
return None
806+
return getattr(parent, attribute)
809807

810808
original = lookup_attribute(parent, attribute)
811809

@@ -817,15 +815,7 @@ def lookup_attribute(parent, attribute):
817815

818816

819817
def apply_patch(parent, attribute, replacement):
820-
try:
821-
setattr(parent, attribute, replacement)
822-
except (TypeError, AttributeError):
823-
# It is a built-in/extension type
824-
# CAVEAT: Global import raises an error, i.e, asynctest package raises:
825-
# 'NoneType' object has no attribute '_spec_coroutines'
826-
from forbiddenfruit import curse
827-
828-
curse(parent, attribute, replacement)
818+
setattr(parent, attribute, replacement)
829819

830820

831821
def wrap_object(module, name, factory, args=(), kwargs={}):
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Remove ``forbiddenfruit`` as dependency and rollback ``wrapt`` changes where ``forbiddenfruit`` was called.
5+
IAST: Patch builtins only when IAST is enabled.
6+

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ def get_exts_for(name):
262262
"xmltodict>=0.12",
263263
"ipaddress; python_version<'3.7'",
264264
"envier",
265-
"forbiddenfruit>=0.1.4",
266265
]
267266
+ bytecode,
268267
extras_require={

0 commit comments

Comments
 (0)