Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 42 additions & 26 deletions sentry_sdk/integrations/aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@
MILLIS_TO_SECONDS = 1000.0


def _get_user_from_event(aws_event: "dict[str, Any]") -> "dict[str, Any]":
if not isinstance(aws_event, dict):
return {}

identity = aws_event.get("requestContext", {}).get("identity")
if identity is None:
return {}

user_info: "dict[str, Any]" = {}

user_arn = identity.get("userArn")
if user_arn is not None:
user_info["id"] = user_arn

ip = identity.get("sourceIp")
if ip is not None:
user_info["ip_address"] = ip

return user_info

Check warning on line 66 in sentry_sdk/integrations/aws_lambda.py

View check run for this annotation

@sentry/warden / warden: find-bugs

Malformed requestContext or identity can abort the Lambda invocation during user extraction

`_get_user_from_event` assumes both `requestContext` and `identity` are dictionaries. When user information collection is enabled (or default PII is enabled), a `None` or non-dictionary value causes an `AttributeError` before the handler is invoked. The issue affects both streaming and non-streaming paths; the initial extraction is outside `capture_internal_exceptions`. Guard both values with `isinstance(..., dict)` or wrap the extraction in `capture_internal_exceptions`.
Comment on lines +48 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_user_from_event crashes when requestContext is None or non-dict

Guard requestContext and identity with isinstance(..., dict) before calling .get(); dict.get("requestContext", {}) still returns None when the key is present, and this helper now runs on the request path outside capture_internal_exceptions.

Evidence
  • _get_user_from_event() does aws_event.get("requestContext", {}).get("identity"), which raises AttributeError if requestContext is explicitly None or otherwise non-dict.
  • After the identity is None check, it calls identity.get(...) with no dict check, so a non-dict identity also raises.
  • The new streaming path calls this helper via scope.set_user(...) outside the nearby capture_internal_exceptions() block, so the exception can fail the Lambda invocation.
  • The same handler already special-cases non-dict headers for this reason, but user extraction does not.

Identified by Warden · code-review · QYJ-NUN

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix attempt detected (commit 99dda7e)

The change adds an aws_event type guard but still calls .get() on requestContext and identity without checking that they are dictionaries, so the reported crashes persist.

The original issue appears unresolved. Please review and try again.

Evaluated by Warden



def _wrap_init_error(init_error: "F") -> "F":
@ensure_integration_enabled(AwsLambdaIntegration, init_error)
def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any":
Expand Down Expand Up @@ -181,6 +202,17 @@
elif should_send_default_pii():
additional_attributes["url.query"] = urlencode(qs)

if not scope._user:
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["user_info"]:
user_info = _get_user_from_event(request_data)
if user_info:
scope.set_user(user_info)
elif should_send_default_pii():
user_info = _get_user_from_event(request_data)
if user_info:
scope.set_user(user_info)
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User extraction can crash handler

Medium Severity

_get_user_from_event now runs on every invocation outside capture_internal_exceptions. It calls .get on requestContext and identity without checking they are dicts, so a non-dict value raises and fails the Lambda invocation before the user handler runs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 99dda7e. Configure here.


sampling_context = {
"aws_event": aws_event,
"aws_context": aws_context,
Expand Down Expand Up @@ -440,38 +472,22 @@
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
user_info = sentry_event.setdefault("user", {})

identity = aws_event.get("requestContext", {}).get("identity")
if identity is None:
identity = {}

id = identity.get("userArn")
if id is not None:
user_info.setdefault("id", id)

ip = identity.get("sourceIp")
if ip is not None:
user_info.setdefault("ip_address", ip)
extracted_user = _get_user_from_event(aws_event)
if extracted_user:
user_info = sentry_event.setdefault("user", {})
for key, value in extracted_user.items():
user_info.setdefault(key, value)

if "incoming_request" in client_options["data_collection"]["http_bodies"]:
if "body" in aws_event:
request["data"] = aws_event.get("body", "")

elif should_send_default_pii():
user_info = sentry_event.setdefault("user", {})

identity = aws_event.get("requestContext", {}).get("identity")
if identity is None:
identity = {}

id = identity.get("userArn")
if id is not None:
user_info.setdefault("id", id)

ip = identity.get("sourceIp")
if ip is not None:
user_info.setdefault("ip_address", ip)
extracted_user = _get_user_from_event(aws_event)
if extracted_user:
user_info = sentry_event.setdefault("user", {})
for key, value in extracted_user.items():
user_info.setdefault(key, value)

if "body" in aws_event:
request["data"] = aws_event.get("body", "")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os

import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration

sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
integrations=[AwsLambdaIntegration()],
trace_lifecycle="stream",
_experiments={
"data_collection": {
"user_info": False,
}
},
)


def handler(event, context):
return {"event": event}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os

import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration

sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
integrations=[AwsLambdaIntegration()],
trace_lifecycle="stream",
_experiments={
"data_collection": {
"user_info": True,
}
},
)


def handler(event, context):
return {"event": event}
74 changes: 74 additions & 0 deletions tests/integrations/aws_lambda/test_aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,80 @@ def test_span_streaming_url_query_params_with_data_collection(
)


def test_span_streaming_user_info_with_send_default_pii(
lambda_client, test_environment
):
payload = b"""
{
"resource": "/asd",
"path": "/asd",
"httpMethod": "GET",
"headers": {
"Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
"User-Agent": "custom",
"X-Forwarded-Proto": "https"
},
"queryStringParameters": {
"bonkers": "true"
},
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"identity": {
"sourceIp": "213.47.147.207",
"userArn": "42"
}
},
"body": null,
"isBase64Encoded": false
}
"""

lambda_client.invoke(
FunctionName="BasicOkSpanStreamingPii",
Payload=payload,
)
span_items = test_environment["server"].span_items

segment_spans = [s for s in span_items if s.get("is_segment")]
assert len(segment_spans) == 1
attrs = segment_spans[0]["attributes"]

assert _get_span_attr(attrs, "user.id") == "42"


def test_span_streaming_user_info_with_data_collection_user_info_on(
lambda_client, test_environment
):
lambda_client.invoke(
FunctionName="BasicOkSpanStreamingDataCollectionUserInfoOn",
Payload=USER_INFO_PAYLOAD,
)
span_items = test_environment["server"].span_items

segment_spans = [s for s in span_items if s.get("is_segment")]
assert len(segment_spans) == 1
attrs = segment_spans[0]["attributes"]

assert _get_span_attr(attrs, "user.id") == "42"


def test_span_streaming_user_info_with_data_collection_user_info_off(
lambda_client, test_environment
):
lambda_client.invoke(
FunctionName="BasicOkSpanStreamingDataCollectionUserInfoOff",
Payload=USER_INFO_PAYLOAD,
)
span_items = test_environment["server"].span_items

segment_spans = [s for s in span_items if s.get("is_segment")]
assert len(segment_spans) == 1
attrs = segment_spans[0]["attributes"]

assert "user.id" not in attrs


@pytest.mark.parametrize(
"lambda_function_name",
["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"],
Expand Down
Loading