Skip to content

Commit

Permalink
Merge branch 'main' into wantsui/changedatetime
Browse files Browse the repository at this point in the history
  • Loading branch information
wantsui authored Jan 9, 2025
2 parents d5680e9 + 5581f73 commit dd7cff4
Show file tree
Hide file tree
Showing 21 changed files with 490 additions and 149 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ benchmarks/base/aspects_benchmarks_generate.py @DataDog/asm-python
ddtrace/appsec/ @DataDog/asm-python
ddtrace/settings/asm.py @DataDog/asm-python
ddtrace/contrib/subprocess/ @DataDog/asm-python
ddtrace/contrib/internal/subprocess/ @DataDog/asm-python
ddtrace/contrib/flask_login/ @DataDog/asm-python
ddtrace/contrib/webbrowser @DataDog/asm-python
ddtrace/contrib/urllib @DataDog/asm-python
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/appsec/_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Flags(enum.IntFlag):
ASM_SESSION_FINGERPRINT = 1 << 33
ASM_NETWORK_FINGERPRINT = 1 << 34
ASM_HEADER_FINGERPRINT = 1 << 35
ASM_RASP_CMDI = 1 << 37


_ALL_ASM_BLOCKING = (
Expand All @@ -49,7 +50,7 @@ class Flags(enum.IntFlag):
| Flags.ASM_HEADER_FINGERPRINT
)

_ALL_RASP = Flags.ASM_RASP_SQLI | Flags.ASM_RASP_LFI | Flags.ASM_RASP_SSRF | Flags.ASM_RASP_SHI
_ALL_RASP = Flags.ASM_RASP_SQLI | Flags.ASM_RASP_LFI | Flags.ASM_RASP_SSRF | Flags.ASM_RASP_SHI | Flags.ASM_RASP_CMDI
_FEATURE_REQUIRED = Flags.ASM_ACTIVATION | Flags.ASM_AUTO_USER


Expand Down
119 changes: 75 additions & 44 deletions ddtrace/appsec/_common_module_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import Union

from wrapt import FunctionWrapper
from wrapt import resolve_path

import ddtrace
from ddtrace.appsec._asm_request_context import get_blocked
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
from ddtrace.appsec._constants import WAF_ACTIONS
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast.constants import VULN_PATH_TRAVERSAL
import ddtrace.contrib.internal.subprocess.patch as subprocess_patch
from ddtrace.internal import core
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal._unpatched import _gc as gc
Expand All @@ -30,6 +34,9 @@

_is_patched = False

_RASP_SYSTEM = "rasp_os.system"
_RASP_POPEN = "rasp_Popen"


def patch_common_modules():
global _is_patched
Expand All @@ -39,7 +46,10 @@ def patch_common_modules():
try_wrap_function_wrapper("urllib.request", "OpenerDirector.open", wrapped_open_ED4CF71136E15EBF)
try_wrap_function_wrapper("_io", "BytesIO.read", wrapped_read_F3E51D71B4EC16EF)
try_wrap_function_wrapper("_io", "StringIO.read", wrapped_read_F3E51D71B4EC16EF)
try_wrap_function_wrapper("os", "system", wrapped_system_5542593D237084A7)
# ensure that the subprocess patch is applied even after one click activation
subprocess_patch.patch()
subprocess_patch.add_str_callback(_RASP_SYSTEM, wrapped_system_5542593D237084A7)
subprocess_patch.add_lst_callback(_RASP_POPEN, popen_FD233052260D8B4D)
core.on("asm.block.dbapi.execute", execute_4C9BAC8E228EB347)
if asm_config._iast_enabled:
_set_metric_iast_instrumented_sink(VULN_PATH_TRAVERSAL)
Expand All @@ -54,6 +64,8 @@ def unpatch_common_modules():
try_unwrap("urllib.request", "OpenerDirector.open")
try_unwrap("_io", "BytesIO.read")
try_unwrap("_io", "StringIO.read")
subprocess_patch.del_str_callback(_RASP_SYSTEM)
subprocess_patch.del_lst_callback(_RASP_POPEN)
_is_patched = False


Expand Down Expand Up @@ -106,7 +118,6 @@ def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -124,7 +135,9 @@ def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs
rule_type=EXPLOIT_PREVENTION.TYPE.LFI,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "lfi", filename)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.LFI, filename
)
try:
return original_open_callable(*args, **kwargs)
except Exception as e:
Expand All @@ -151,7 +164,6 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -168,7 +180,9 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
rule_type=EXPLOIT_PREVENTION.TYPE.SSRF,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "ssrf", url)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, url
)
return original_open_callable(*args, **kwargs)


Expand All @@ -191,7 +205,6 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -206,50 +219,67 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
rule_type=EXPLOIT_PREVENTION.TYPE.SSRF,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "ssrf", url)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SSRF, url
)

return original_request_callable(*args, **kwargs)


def wrapped_system_5542593D237084A7(original_command_callable, instance, args, kwargs):
def wrapped_system_5542593D237084A7(command: str) -> None:
"""
wrapper for os.system function
"""
command = args[0] if args else kwargs.get("command", None)
if command is not None:
if asm_config._iast_enabled and is_iast_request_enabled():
from ddtrace.appsec._iast.taint_sinks.command_injection import _iast_report_cmdi

_iast_report_cmdi(command)

if (
asm_config._asm_enabled
and asm_config._ep_enabled
and ddtrace.tracer._appsec_processor is not None
and ddtrace.tracer._appsec_processor.rasp_cmdi_enabled
):
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
return original_command_callable(*args, **kwargs)

if in_asm_context():
res = call_waf_callback(
{EXPLOIT_PREVENTION.ADDRESS.CMDI: command},
crop_trace="wrapped_system_5542593D237084A7",
rule_type=EXPLOIT_PREVENTION.TYPE.CMDI,
if (
asm_config._asm_enabled
and asm_config._ep_enabled
and ddtrace.tracer._appsec_processor is not None
and ddtrace.tracer._appsec_processor.rasp_shi_enabled
):
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
except ImportError:
return

if in_asm_context():
res = call_waf_callback(
{EXPLOIT_PREVENTION.ADDRESS.SHI: command},
crop_trace="wrapped_system_5542593D237084A7",
rule_type=EXPLOIT_PREVENTION.TYPE.SHI,
)
if res and _must_block(res.actions):
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SHI, command
)


def popen_FD233052260D8B4D(arg_list: Union[List[str], str]) -> None:
"""
listener for subprocess.Popen class
"""
if (
asm_config._asm_enabled
and asm_config._ep_enabled
and ddtrace.tracer._appsec_processor is not None
and ddtrace.tracer._appsec_processor.rasp_cmdi_enabled
):
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
except ImportError:
return

if in_asm_context():
res = call_waf_callback(
{EXPLOIT_PREVENTION.ADDRESS.CMDI: arg_list if isinstance(arg_list, list) else [arg_list]},
crop_trace="popen_FD233052260D8B4D",
rule_type=EXPLOIT_PREVENTION.TYPE.CMDI,
)
if res and _must_block(res.actions):
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.CMDI, arg_list
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "cmdi", command)
try:
return original_command_callable(*args, **kwargs)
except Exception as e:
previous_frame = e.__traceback__.tb_frame.f_back
raise e.with_traceback(
e.__traceback__.__class__(None, previous_frame, previous_frame.f_lasti, previous_frame.f_lineno)
)


_DB_DIALECTS = {
Expand Down Expand Up @@ -279,7 +309,6 @@ def execute_4C9BAC8E228EB347(instrument_self, query, args, kwargs) -> None:
try:
from ddtrace.appsec._asm_request_context import call_waf_callback
from ddtrace.appsec._asm_request_context import in_asm_context
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# execute is used during module initialization
# and shouldn't be changed at that time
Expand All @@ -296,7 +325,9 @@ def execute_4C9BAC8E228EB347(instrument_self, query, args, kwargs) -> None:
rule_type=EXPLOIT_PREVENTION.TYPE.SQLI,
)
if res and _must_block(res.actions):
raise BlockingException(get_blocked(), "exploit_prevention", "sqli", query)
raise BlockingException(
get_blocked(), EXPLOIT_PREVENTION.BLOCKING, EXPLOIT_PREVENTION.TYPE.SQLI, query
)


def try_unwrap(module, name):
Expand Down
6 changes: 5 additions & 1 deletion ddtrace/appsec/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ class WAF_DATA_NAMES(metaclass=Constant_Class):

# EPHEMERAL ADDRESSES
PROCESSOR_SETTINGS: Literal["waf.context.processor"] = "waf.context.processor"
CMDI_ADDRESS: Literal["server.sys.shell.cmd"] = "server.sys.shell.cmd"
CMDI_ADDRESS: Literal["server.sys.exec.cmd"] = "server.sys.exec.cmd"
SHI_ADDRESS: Literal["server.sys.shell.cmd"] = "server.sys.shell.cmd"
LFI_ADDRESS: Literal["server.io.fs.file"] = "server.io.fs.file"
SSRF_ADDRESS: Literal["server.io.net.url"] = "server.io.net.url"
SQLI_ADDRESS: Literal["server.db.statement"] = "server.db.statement"
Expand Down Expand Up @@ -328,6 +329,7 @@ class DEFAULT(metaclass=Constant_Class):


class EXPLOIT_PREVENTION(metaclass=Constant_Class):
BLOCKING: Literal["exploit_prevention"] = "exploit_prevention"
STACK_TRACE_ID: Literal["stack_id"] = "stack_id"
EP_ENABLED: Literal["DD_APPSEC_RASP_ENABLED"] = "DD_APPSEC_RASP_ENABLED"
STACK_TRACE_ENABLED: Literal["DD_APPSEC_STACK_TRACE_ENABLED"] = "DD_APPSEC_STACK_TRACE_ENABLED"
Expand All @@ -339,13 +341,15 @@ class EXPLOIT_PREVENTION(metaclass=Constant_Class):

class TYPE(metaclass=Constant_Class):
CMDI: Literal["command_injection"] = "command_injection"
SHI: Literal["shell_injection"] = "shell_injection"
LFI: Literal["lfi"] = "lfi"
SSRF: Literal["ssrf"] = "ssrf"
SQLI: Literal["sql_injection"] = "sql_injection"

class ADDRESS(metaclass=Constant_Class):
CMDI: Literal["CMDI_ADDRESS"] = "CMDI_ADDRESS"
LFI: Literal["LFI_ADDRESS"] = "LFI_ADDRESS"
SHI: Literal["SHI_ADDRESS"] = "SHI_ADDRESS"
SSRF: Literal["SSRF_ADDRESS"] = "SSRF_ADDRESS"
SQLI: Literal["SQLI_ADDRESS"] = "SQLI_ADDRESS"
SQLI_TYPE: Literal["SQLI_SYSTEM_ADDRESS"] = "SQLI_SYSTEM_ADDRESS"
Expand Down
2 changes: 2 additions & 0 deletions ddtrace/appsec/_iast/_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def ddtrace_iast(request, ddspan):
Optionally output the test as failed if vulnerabilities are found.
"""
yield
if ddspan is None:
return
data = ddspan.get_tag(IAST.JSON)
if not data:
return
Expand Down
51 changes: 10 additions & 41 deletions ddtrace/appsec/_iast/taint_sinks/command_injection.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import os
import subprocess # nosec
from typing import List
from typing import Union

from ddtrace.appsec._common_module_patches import try_unwrap
from ddtrace.appsec._constants import IAST_SPAN_TAGS
from ddtrace.appsec._iast import oce
from ddtrace.appsec._iast._iast_request_context import is_iast_request_enabled
from ddtrace.appsec._iast._metrics import _set_metric_iast_executed_sink
from ddtrace.appsec._iast._metrics import _set_metric_iast_instrumented_sink
from ddtrace.appsec._iast._metrics import increment_iast_span_metric
from ddtrace.appsec._iast._patch import try_wrap_function_wrapper
from ddtrace.appsec._iast._taint_tracking._taint_objects import is_pyobject_tainted
from ddtrace.appsec._iast.constants import VULN_CMDI
import ddtrace.contrib.internal.subprocess.patch as subprocess_patch
from ddtrace.internal.logger import get_logger
from ddtrace.settings.asm import config as asm_config

Expand All @@ -26,48 +23,20 @@ def get_version() -> str:
return ""


def patch():
if not asm_config._iast_enabled:
return

if not getattr(os, "_datadog_cmdi_patch", False):
# all os.spawn* variants eventually use this one:
try_wrap_function_wrapper("os", "_spawnvef", _iast_cmdi_osspawn)

if not getattr(subprocess, "_datadog_cmdi_patch", False):
try_wrap_function_wrapper("subprocess", "Popen.__init__", _iast_cmdi_subprocess_init)
_IAST_CMDI = "iast_cmdi"

os._datadog_cmdi_patch = True
subprocess._datadog_cmdi_patch = True

_set_metric_iast_instrumented_sink(VULN_CMDI)
def patch():
if asm_config._iast_enabled:
subprocess_patch.patch()
subprocess_patch.add_str_callback(_IAST_CMDI, _iast_report_cmdi)
subprocess_patch.add_lst_callback(_IAST_CMDI, _iast_report_cmdi)
_set_metric_iast_instrumented_sink(VULN_CMDI)


def unpatch() -> None:
try_unwrap("os", "system")
try_unwrap("os", "_spawnvef")
try_unwrap("subprocess", "Popen.__init__")

os._datadog_cmdi_patch = False # type: ignore[attr-defined]
subprocess._datadog_cmdi_patch = False # type: ignore[attr-defined]


def _iast_cmdi_osspawn(wrapped, instance, args, kwargs):
mode, file, func_args, _, _ = args
_iast_report_cmdi(func_args)

if hasattr(wrapped, "__func__"):
return wrapped.__func__(instance, *args, **kwargs)
return wrapped(*args, **kwargs)


def _iast_cmdi_subprocess_init(wrapped, instance, args, kwargs):
cmd_args = args[0] if len(args) else kwargs["args"]
_iast_report_cmdi(cmd_args)

if hasattr(wrapped, "__func__"):
return wrapped.__func__(instance, *args, **kwargs)
return wrapped(*args, **kwargs)
subprocess_patch.del_str_callback(_IAST_CMDI)
subprocess_patch.del_lst_callback(_IAST_CMDI)


@oce.register
Expand Down
Loading

0 comments on commit dd7cff4

Please sign in to comment.