@@ -747,7 +747,11 @@ def test_transmit_http_error_not_retryable(self):
747747 def test_transmit_http_error_redirect (self ):
748748 response = HttpResponse (None , None )
749749 response .status_code = 307
750- response .headers = {"location" : "https://example.com" }
750+ # Redirect target whose host differs from the default ingestion host
751+ # (`dc.services.visualstudio.com`) only in the leftmost DNS label, with
752+ # the same number of labels and a 3-label shared suffix, so the
753+ # cross-origin redirect guard permits it.
754+ response .headers = {"location" : "https://westus.services.visualstudio.com" }
751755 prev_redirects = self ._base .client ._config .redirect_policy .max_redirects
752756 self ._base .client ._config .redirect_policy .max_redirects = 2
753757 prev_host = self ._base .client ._config .host
@@ -757,10 +761,57 @@ def test_transmit_http_error_redirect(self):
757761 result = self ._base ._transmit (self ._envelopes_to_export )
758762 self .assertEqual (result , ExportResult .FAILED_NOT_RETRYABLE )
759763 self .assertEqual (post .call_count , 2 )
760- self .assertEqual (self ._base .client ._config .host , "https://example.com" )
764+ self .assertEqual (
765+ self ._base .client ._config .host ,
766+ "https://westus.services.visualstudio.com" ,
767+ )
761768 self ._base .client ._config .redirect_policy .max_redirects = prev_redirects
762769 self ._base .client ._config .host = prev_host
763770
771+ def test_transmit_http_error_redirect_refuses_cross_origin (self ):
772+ """A redirect to a different registered domain must be refused so the
773+ auth policy does not attach a bearer token for a foreign host on the
774+ recursive _transmit call."""
775+ response = HttpResponse (None , None )
776+ response .status_code = 307
777+ response .headers = {"location" : "https://attacker.example.com" }
778+ prev_host = self ._base .client ._config .host
779+ error = HttpResponseError (response = response )
780+ with mock .patch .object (AzureMonitorClient , "track" ) as post :
781+ post .side_effect = error
782+ result = self ._base ._transmit (self ._envelopes_to_export )
783+ self .assertEqual (result , ExportResult .FAILED_NOT_RETRYABLE )
784+ self .assertEqual (post .call_count , 1 )
785+ self .assertEqual (self ._base .client ._config .host , prev_host )
786+
787+ def test_is_same_registered_domain (self ):
788+ same = self ._base ._is_same_registered_domain
789+ # Same host (exact match) is always safe, even when not under a
790+ # trusted ingestion suffix (e.g. customer-configured custom host).
791+ self .assertTrue (same ("westus-0.in.applicationinsights.azure.com" , "westus-0.in.applicationinsights.azure.com" ))
792+ self .assertTrue (same ("custom-ingestion.example.invalid" , "custom-ingestion.example.invalid" ))
793+ # Both hosts under the same trusted Azure Monitor ingestion suffix
794+ # are permitted -- this is the cross-region case.
795+ self .assertTrue (same ("westus-0.in.applicationinsights.azure.com" , "eastus-0.in.applicationinsights.azure.com" ))
796+ self .assertTrue (same ("dc.services.visualstudio.com" , "westus.services.visualstudio.com" ))
797+ self .assertTrue (same ("foo.applicationinsights.azure.us" , "bar.applicationinsights.azure.us" ))
798+ # Different registered domain entirely is rejected.
799+ self .assertFalse (same ("westus-0.in.applicationinsights.azure.com" , "attacker.com" ))
800+ self .assertFalse (same ("foo.example.com" , "foo.example.org" ))
801+ # Sibling subdomains under an untrusted parent are rejected -- this
802+ # is the cross-origin-leak PoC scenario.
803+ self .assertFalse (same ("legit-ingestion.example.invalid" , "attacker.example.invalid" ))
804+ self .assertFalse (same ("foo.azure.com" , "bar.azure.com" ))
805+ # A trusted host cannot be redirected to a host under an untrusted
806+ # suffix (and vice versa).
807+ self .assertFalse (same ("dc.services.visualstudio.com" , "attacker.example.invalid" ))
808+ self .assertFalse (same ("legit-ingestion.example.invalid" , "dc.services.visualstudio.com" ))
809+ # Mixing two different trusted suffixes is rejected.
810+ self .assertFalse (same ("dc.services.visualstudio.com" , "westus-0.in.applicationinsights.azure.com" ))
811+ # Empty inputs are treated as not-same.
812+ self .assertFalse (same ("" , "applicationinsights.azure.com" ))
813+ self .assertFalse (same ("applicationinsights.azure.com" , "" ))
814+
764815 def test_transmit_http_error_redirect_missing_headers (self ):
765816 response = HttpResponse (None , None )
766817 response .status_code = 307
0 commit comments