@@ -858,6 +858,256 @@ def test_get_request_url_x_forwarded_proto(environ, use_x_forwarded_for, expecte
858858 assert get_request_url (environ , use_x_forwarded_for ) == expected_url
859859
860860
861+ @pytest .mark .parametrize ("send_default_pii" , [True , False ])
862+ def test_request_headers_data_collection_default_redacts_sensitive (
863+ sentry_init , crashing_app , capture_events , send_default_pii
864+ ):
865+ """
866+ When ``data_collection`` is configured (even as ``None``, i.e. spec
867+ defaults), the WSGI event processor routes request headers through the
868+ data-collection filtering path. Sensitive headers are redacted regardless
869+ of ``send_default_pii`` -- the value of that legacy option must not change
870+ the outcome.
871+ """
872+ sentry_init (
873+ send_default_pii = send_default_pii ,
874+ _experiments = {"data_collection" : None },
875+ )
876+ app = SentryWsgiMiddleware (crashing_app )
877+ client = Client (app )
878+ events = capture_events ()
879+
880+ with pytest .raises (ZeroDivisionError ):
881+ client .get (
882+ "/" ,
883+ headers = {
884+ "Authorization" : "Bearer secret-token" ,
885+ "X-Custom-Header" : "passthrough" ,
886+ },
887+ )
888+
889+ (event ,) = events
890+ headers = event ["request" ]["headers" ]
891+
892+ assert headers ["Authorization" ] == "[Filtered]"
893+ assert headers ["X-Custom-Header" ] == "passthrough"
894+
895+
896+ def test_request_headers_legacy_no_pii_redacts_sensitive (
897+ sentry_init , crashing_app , capture_events
898+ ):
899+ """
900+ With no ``data_collection`` configured, ``_filter_headers`` falls back to
901+ the legacy ``send_default_pii`` behaviour. When PII is disabled, headers in
902+ ``SENSITIVE_HEADERS`` are replaced with an ``AnnotatedValue`` (the default
903+ ``use_annotated_value=True`` on the event-processor call site), which
904+ serializes to an emptied value plus a ``_meta`` annotation. Non-sensitive
905+ headers pass through untouched.
906+
907+ ``X-Forwarded-For`` is used because it is in ``SENSITIVE_HEADERS`` but is
908+ not scrubbed by the default ``EventScrubber``, so the substitution we are
909+ asserting on can only come from ``_filter_headers``.
910+ """
911+ sentry_init (send_default_pii = False )
912+ app = SentryWsgiMiddleware (crashing_app )
913+ client = Client (app )
914+ events = capture_events ()
915+
916+ with pytest .raises (ZeroDivisionError ):
917+ client .get (
918+ "/" ,
919+ headers = {
920+ "X-Forwarded-For" : "1.2.3.4" ,
921+ "X-Custom-Header" : "passthrough" ,
922+ },
923+ )
924+
925+ (event ,) = events
926+
927+ assert event ["request" ]["headers" ]["X-Forwarded-For" ] == ""
928+ assert event ["request" ]["headers" ]["X-Custom-Header" ] == "passthrough"
929+
930+ # The emptied value is accompanied by a `_meta` annotation marking it as
931+ # removed, confirming the substitution came from the AnnotatedValue path.
932+ assert event ["_meta" ]["request" ]["headers" ]["X-Forwarded-For" ] == {
933+ "" : {"rem" : [["!config" , "x" ]]}
934+ }
935+
936+
937+ def test_request_headers_data_collection_off_collects_no_headers (
938+ sentry_init , crashing_app , capture_events
939+ ):
940+ """
941+ With ``http_headers.request`` mode set to ``off``, no request headers are
942+ collected at all -- the filtering returns an empty mapping.
943+ """
944+ sentry_init (
945+ _experiments = {
946+ "data_collection" : {"http_headers" : {"request" : {"mode" : "off" }}}
947+ },
948+ )
949+ app = SentryWsgiMiddleware (crashing_app )
950+ client = Client (app )
951+ events = capture_events ()
952+
953+ with pytest .raises (ZeroDivisionError ):
954+ client .get (
955+ "/" ,
956+ headers = {
957+ "X-Forwarded-For" : "1.2.3.4" ,
958+ "X-Custom-Header" : "passthrough" ,
959+ },
960+ )
961+
962+ (event ,) = events
963+
964+ assert event ["request" ]["headers" ] == {}
965+
966+
967+ def test_request_headers_data_collection_allowlist_redacts_all_but_allowed_terms (
968+ sentry_init , crashing_app , capture_events
969+ ):
970+ """
971+ An ``allowlist`` allows through only headers matching a configured term
972+ (partial, case-insensitive); every other header key is kept but its value
973+ is redacted.
974+ """
975+ sentry_init (
976+ _experiments = {
977+ "data_collection" : {
978+ "http_headers" : {"request" : {"mode" : "allowlist" , "terms" : ["custom" ]}}
979+ }
980+ },
981+ )
982+ app = SentryWsgiMiddleware (crashing_app )
983+ client = Client (app )
984+ events = capture_events ()
985+
986+ with pytest .raises (ZeroDivisionError ):
987+ client .get (
988+ "/" ,
989+ headers = {
990+ "X-Forwarded-For" : "1.2.3.4" ,
991+ "X-Custom-Header" : "passthrough" ,
992+ },
993+ )
994+
995+ (event ,) = events
996+ headers = event ["request" ]["headers" ]
997+
998+ assert headers ["X-Custom-Header" ] == "passthrough"
999+ assert headers ["X-Forwarded-For" ] == "[Filtered]"
1000+ assert headers ["Host" ] == "[Filtered]"
1001+
1002+
1003+ def test_request_headers_data_collection_denylist_redacts_only_matched_terms (
1004+ sentry_init , crashing_app , capture_events
1005+ ):
1006+ """
1007+ A ``denylist`` passes headers through by default, redacting only those
1008+ matching a configured term (partial, case-insensitive).
1009+ """
1010+ sentry_init (
1011+ _experiments = {
1012+ "data_collection" : {
1013+ "http_headers" : {"request" : {"mode" : "denylist" , "terms" : ["custom" ]}}
1014+ }
1015+ },
1016+ )
1017+ app = SentryWsgiMiddleware (crashing_app )
1018+ client = Client (app )
1019+ events = capture_events ()
1020+
1021+ with pytest .raises (ZeroDivisionError ):
1022+ client .get (
1023+ "/" ,
1024+ headers = {
1025+ "X-Forwarded-For" : "1.2.3.4" ,
1026+ "X-Custom-Header" : "passthrough" ,
1027+ },
1028+ )
1029+
1030+ (event ,) = events
1031+ headers = event ["request" ]["headers" ]
1032+
1033+ assert headers ["X-Custom-Header" ] == "[Filtered]"
1034+ assert headers ["X-Forwarded-For" ] == "1.2.3.4"
1035+ assert headers ["Host" ] == "localhost"
1036+
1037+
1038+ def test_request_headers_data_collection_cookie_always_redacted (
1039+ sentry_init , crashing_app , capture_events
1040+ ):
1041+ """
1042+ The ``cookie``/``set-cookie`` headers are always redacted in the
1043+ data-collection path, even when explicitly allowlisted. A sibling header
1044+ (``custom``) that is also allowlisted passes through, isolating the
1045+ cookie override.
1046+
1047+ The middleware is driven with an explicit environ because werkzeug's test
1048+ ``Client`` manages its own cookie jar and strips the ``Cookie`` header.
1049+ """
1050+ sentry_init (
1051+ _experiments = {
1052+ "data_collection" : {
1053+ "http_headers" : {
1054+ "request" : {"mode" : "allowlist" , "terms" : ["cookie" , "custom" ]}
1055+ }
1056+ }
1057+ },
1058+ )
1059+ app = SentryWsgiMiddleware (crashing_app )
1060+ events = capture_events ()
1061+
1062+ environ = {
1063+ "REQUEST_METHOD" : "GET" ,
1064+ "PATH_INFO" : "/" ,
1065+ "SERVER_NAME" : "localhost" ,
1066+ "SERVER_PORT" : "80" ,
1067+ "wsgi.url_scheme" : "http" ,
1068+ "HTTP_COOKIE" : "sessionid=secret" ,
1069+ "HTTP_X_CUSTOM_HEADER" : "passthrough" ,
1070+ }
1071+
1072+ with pytest .raises (ZeroDivisionError ):
1073+ list (app (environ , lambda status , headers : None ))
1074+
1075+ (event ,) = events
1076+ headers = event ["request" ]["headers" ]
1077+
1078+ assert headers ["Cookie" ] == "[Filtered]"
1079+ assert headers ["X-Custom-Header" ] == "passthrough"
1080+
1081+
1082+ def test_request_headers_legacy_pii_passes_headers_through (
1083+ sentry_init , crashing_app , capture_events
1084+ ):
1085+ """
1086+ With no ``data_collection`` configured and ``send_default_pii`` enabled,
1087+ the legacy path returns all headers unchanged -- including those in
1088+ ``SENSITIVE_HEADERS``.
1089+ """
1090+ sentry_init (send_default_pii = True )
1091+ app = SentryWsgiMiddleware (crashing_app )
1092+ client = Client (app )
1093+ events = capture_events ()
1094+
1095+ with pytest .raises (ZeroDivisionError ):
1096+ client .get (
1097+ "/" ,
1098+ headers = {
1099+ "X-Forwarded-For" : "1.2.3.4" ,
1100+ "X-Custom-Header" : "passthrough" ,
1101+ },
1102+ )
1103+
1104+ (event ,) = events
1105+ headers = event ["request" ]["headers" ]
1106+
1107+ assert headers ["X-Forwarded-For" ] == "1.2.3.4"
1108+ assert headers ["X-Custom-Header" ] == "passthrough"
1109+
1110+
8611111@pytest .mark .parametrize ("send_default_pii" , [True , False ])
8621112def test_user_ip_address_on_all_spans (sentry_init , capture_items , send_default_pii ):
8631113 def dogpark (environ , start_response ):
0 commit comments