Skip to content

Commit

Permalink
Merge branch 'main' into APPSEC-52730_strip_headers_from_blocked_resp…
Browse files Browse the repository at this point in the history
…onses
  • Loading branch information
christophe-papazian authored May 2, 2024
2 parents f8692c0 + 01fbf91 commit a1ccfa4
Show file tree
Hide file tree
Showing 15 changed files with 218 additions and 20 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ ddtrace/appsec/ @DataDog/asm-python
ddtrace/settings/asm.py @DataDog/asm-python
ddtrace/contrib/subprocess/ @DataDog/asm-python
ddtrace/contrib/flask_login/ @DataDog/asm-python
ddtrace/internal/_exceptions.py @DataDog/asm-python
tests/appsec/ @DataDog/asm-python
tests/contrib/dbapi/test_dbapi_appsec.py @DataDog/asm-python
tests/contrib/subprocess @DataDog/asm-python
Expand Down
8 changes: 8 additions & 0 deletions ddtrace/appsec/_asm_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ddtrace.appsec._iast._utils import _is_iast_enabled
from ddtrace.appsec._utils import get_triggers
from ddtrace.internal import core
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal.constants import REQUEST_PATH_PARAMS
from ddtrace.internal.logger import get_logger
from ddtrace.settings.asm import config as asm_config
Expand Down Expand Up @@ -140,6 +141,7 @@ def __init__(self):
env = ASM_Environment(True)

self._id = _DataHandler.main_id
self._root = not in_context()
self.active = True
self.execution_context = core.ExecutionContext(__name__, **{"asm_env": env})

Expand Down Expand Up @@ -393,6 +395,12 @@ def asm_request_context_manager(
if resources is not None:
try:
yield resources
except BlockingException as e:
# ensure that the BlockingRequest that is never raised outside a context
# is also never propagated outside the context
core.set_item(WAF_CONTEXT_NAMES.BLOCKED, e.args[0])
if not resources._root:
raise
finally:
_end_context(resources)
else:
Expand Down
16 changes: 13 additions & 3 deletions ddtrace/appsec/_common_module_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from typing import Callable
from typing import Dict

from ddtrace.appsec._constants import WAF_CONTEXT_NAMES
from ddtrace.internal import core
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal.logger import get_logger
from ddtrace.settings.asm import config as asm_config
from ddtrace.vendor.wrapt import FunctionWrapper
Expand Down Expand Up @@ -49,6 +51,7 @@ 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_context
from ddtrace.appsec._asm_request_context import is_blocked
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
Expand All @@ -66,7 +69,9 @@ def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs
crop_trace="wrapped_open_CFDDB7ABBA9081B6",
rule_type=EXPLOIT_PREVENTION.TYPE.LFI,
)
# DEV: Next part of the exploit prevention feature: add block here
if is_blocked():
raise BlockingException(core.get_item(WAF_CONTEXT_NAMES.BLOCKED), "exploit_prevention", "lfi", filename)

return original_open_callable(*args, **kwargs)


Expand All @@ -82,6 +87,7 @@ 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_context
from ddtrace.appsec._asm_request_context import is_blocked
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
Expand All @@ -98,7 +104,8 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
crop_trace="wrapped_open_ED4CF71136E15EBF",
rule_type=EXPLOIT_PREVENTION.TYPE.SSRF,
)
# DEV: Next part of the exploit prevention feature: add block here
if is_blocked():
raise BlockingException(core.get_item(WAF_CONTEXT_NAMES.BLOCKED), "exploit_prevention", "ssrf", url)
return original_open_callable(*args, **kwargs)


Expand All @@ -115,6 +122,7 @@ 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_context
from ddtrace.appsec._asm_request_context import is_blocked
from ddtrace.appsec._constants import EXPLOIT_PREVENTION
except ImportError:
# open is used during module initialization
Expand All @@ -129,7 +137,9 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
crop_trace="wrapped_request_D8CB81E472AF98A2",
rule_type=EXPLOIT_PREVENTION.TYPE.SSRF,
)
# DEV: Next part of the exploit prevention feature: add block here
if is_blocked():
raise BlockingException(core.get_item(WAF_CONTEXT_NAMES.BLOCKED), "exploit_prevention", "ssrf", url)

return original_request_callable(*args, **kwargs)


Expand Down
4 changes: 4 additions & 0 deletions ddtrace/contrib/asgi/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ddtrace.ext import SpanKind
from ddtrace.ext import SpanTypes
from ddtrace.ext import http
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal.compat import is_valid_ip
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.constants import HTTP_REQUEST_BLOCKED
Expand Down Expand Up @@ -288,6 +289,9 @@ async def wrapped_blocked_send(message):
try:
core.dispatch("asgi.start_request", ("asgi",))
return await self.app(scope, receive, wrapped_send)
except BlockingException as e:
core.set_item(HTTP_REQUEST_BLOCKED, e.args[0])
return await _blocked_asgi_app(scope, receive, wrapped_blocked_send)
except trace_utils.InterruptException:
return await _blocked_asgi_app(scope, receive, wrapped_blocked_send)
except Exception as exc:
Expand Down
10 changes: 8 additions & 2 deletions ddtrace/contrib/django/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ddtrace.ext import http
from ddtrace.ext import sql as sqlx
from ddtrace.internal import core
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal.compat import Iterable
from ddtrace.internal.compat import maybe_stringify
from ddtrace.internal.constants import COMPONENT
Expand Down Expand Up @@ -467,7 +468,7 @@ def traced_get_response(django, pin, func, instance, args, kwargs):
def blocked_response():
from django.http import HttpResponse

block_config = core.get_item(HTTP_REQUEST_BLOCKED)
block_config = core.get_item(HTTP_REQUEST_BLOCKED) or {}
desired_type = block_config.get("type", "auto")
status = block_config.get("status_code", 403)
if desired_type == "none":
Expand Down Expand Up @@ -511,7 +512,12 @@ def blocked_response():
response = blocked_response()
return response

response = func(*args, **kwargs)
try:
response = func(*args, **kwargs)
except BlockingException as e:
core.set_item(HTTP_REQUEST_BLOCKED, e.args[0])
response = blocked_response()
return response

if core.get_item(HTTP_REQUEST_BLOCKED):
response = blocked_response()
Expand Down
22 changes: 13 additions & 9 deletions ddtrace/contrib/wsgi/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from ddtrace.contrib import trace_utils
from ddtrace.ext import SpanKind
from ddtrace.ext import SpanTypes
from ddtrace.internal._exceptions import BlockingException
from ddtrace.internal.constants import COMPONENT
from ddtrace.internal.constants import HTTP_REQUEST_BLOCKED
from ddtrace.internal.logger import get_logger
Expand Down Expand Up @@ -109,15 +110,6 @@ def __call__(self, environ: Iterable, start_response: Callable) -> wrapt.ObjectP
call_key="req_span",
) as ctx:
ctx.set_item("wsgi.construct_url", construct_url)
if core.get_item(HTTP_REQUEST_BLOCKED):
result = core.dispatch_with_results("wsgi.block.started", (ctx, construct_url)).status_headers_content
if result:
status, headers, content = result.value
else:
status, headers, content = 403, [], ""
start_response(str(status), headers)
closing_iterable = [content]
not_blocked = False

def blocked_view():
result = core.dispatch_with_results("wsgi.block.started", (ctx, construct_url)).status_headers_content
Expand All @@ -127,12 +119,24 @@ def blocked_view():
status, headers, content = 403, [], ""
return content, status, headers

if core.get_item(HTTP_REQUEST_BLOCKED):
content, status, headers = blocked_view()
start_response(str(status), headers)
closing_iterable = [content]
not_blocked = False

core.dispatch("wsgi.block_decided", (blocked_view,))

if not_blocked:
core.dispatch("wsgi.request.prepare", (ctx, start_response))
try:
closing_iterable = self.app(environ, ctx.get_item("intercept_start_response"))
except BlockingException as e:
core.set_item(HTTP_REQUEST_BLOCKED, e.args[0])
content, status, headers = blocked_view()
start_response(str(status), headers)
closing_iterable = [content]
core.dispatch("wsgi.app.exception", (ctx,))
except BaseException:
core.dispatch("wsgi.app.exception", (ctx,))
raise
Expand Down
5 changes: 5 additions & 0 deletions ddtrace/internal/_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class BlockingException(BaseException):
"""
Exception raised when a request is blocked by ASM
It derives from BaseException to avoid being caught by the general Exception handler
"""
1 change: 1 addition & 0 deletions tests/.suitespec.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
],
"core": [
"ddtrace/internal/__init__.py",
"ddtrace/internal/_exceptions.py",
"ddtrace/internal/_rand.pyi",
"ddtrace/internal/_rand.pyx",
"ddtrace/internal/_stdint.h",
Expand Down
106 changes: 106 additions & 0 deletions tests/appsec/appsec/rules-rasp-blocking.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"version": "2.1",
"metadata": {
"rules_version": "rules_rasp"
},
"rules": [
{
"id": "rasp-930-100",
"name": "Local file inclusion exploit",
"tags": {
"type": "lfi",
"category": "vulnerability_trigger",
"cwe": "22",
"capec": "1000/255/153/126",
"confidence": "0",
"module": "rasp"
},
"conditions": [
{
"parameters": {
"resource": [
{
"address": "server.io.fs.file"
}
],
"params": [
{
"address": "server.request.query"
},
{
"address": "server.request.body"
},
{
"address": "server.request.path_params"
},
{
"address": "grpc.server.request.message"
},
{
"address": "graphql.server.all_resolvers"
},
{
"address": "graphql.server.resolver"
}
]
},
"operator": "lfi_detector"
}
],
"transformers": [],
"on_match": [
"stack_trace",
"block"
]
},
{
"id": "rasp-934-100",
"name": "Server-side request forgery exploit",
"tags": {
"type": "ssrf",
"category": "vulnerability_trigger",
"cwe": "918",
"capec": "1000/225/115/664",
"confidence": "0",
"module": "rasp"
},
"conditions": [
{
"parameters": {
"resource": [
{
"address": "server.io.net.url"
}
],
"params": [
{
"address": "server.request.query"
},
{
"address": "server.request.body"
},
{
"address": "server.request.path_params"
},
{
"address": "grpc.server.request.message"
},
{
"address": "graphql.server.all_resolvers"
},
{
"address": "graphql.server.resolver"
}
]
},
"operator": "ssrf_detector"
}
],
"transformers": [],
"on_match": [
"stack_trace",
"block"
]
}
]
}
17 changes: 17 additions & 0 deletions tests/appsec/appsec/test_asm_request_context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from ddtrace.appsec import _asm_request_context
from ddtrace.internal._exceptions import BlockingException
from tests.utils import override_global_config


Expand Down Expand Up @@ -94,3 +95,19 @@ def test_asm_request_context_manager():
assert _asm_request_context.get_headers() == {}
assert _asm_request_context.get_value("callbacks", "block") is None
assert not _asm_request_context.get_headers_case_sensitive()


def test_blocking_exception_correctly_propagated():
with override_global_config({"_asm_enabled": True}):
with _asm_request_context.asm_request_context_manager():
witness = 0
with _asm_request_context.asm_request_context_manager():
witness = 1
raise BlockingException({}, "rule", "type", "value")
# should be skipped by exception
witness = 3
# should be also skipped by exception
witness = 4
# no more exception there
# ensure that the exception was raised and caught at the end of the last context manager
assert witness == 1
3 changes: 3 additions & 0 deletions tests/appsec/contrib_appsec/django_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def rasp(request, endpoint: str):
res.append(f"File: {f.read()}")
except Exception as e:
res.append(f"Error: {e}")
tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint)
return HttpResponse("<\br>\n".join(res))
elif endpoint == "ssrf":
res = ["ssrf endpoint"]
Expand Down Expand Up @@ -98,7 +99,9 @@ def rasp(request, endpoint: str):
res.append(f"Url: {r.text}")
except Exception as e:
res.append(f"Error: {e}")
tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint)
return HttpResponse("<\\br>\n".join(res))
tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint)
return HttpResponse(f"Unknown endpoint: {endpoint}")


Expand Down
3 changes: 3 additions & 0 deletions tests/appsec/contrib_appsec/fastapi_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async def rasp(endpoint: str, request: Request):
res.append(f"File: {f.read()}")
except Exception as e:
res.append(f"Error: {e}")
tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint)
return HTMLResponse("<\br>\n".join(res))
elif endpoint == "ssrf":
res = ["ssrf endpoint"]
Expand Down Expand Up @@ -155,7 +156,9 @@ async def rasp(endpoint: str, request: Request):
res.append(f"Url: {r.text}")
except Exception as e:
res.append(f"Error: {e}")
tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint)
return HTMLResponse("<\\br>\n".join(res))
tracer.current_span()._local_root.set_tag("rasp.request.done", endpoint)
return HTMLResponse(f"Unknown endpoint: {endpoint}")

return app
Loading

0 comments on commit a1ccfa4

Please sign in to comment.