1
+ import hashlib
2
+ import hmac
1
3
import ipaddress
2
4
import logging
3
5
from abc import ABC
15
17
from sentry .api .api_owners import ApiOwner
16
18
from sentry .api .api_publish_status import ApiPublishStatus
17
19
from sentry .api .base import Endpoint , region_silo_endpoint
20
+ from sentry .api .exceptions import SentryAPIException
18
21
from sentry .integrations .base import IntegrationDomain
19
22
from sentry .integrations .bitbucket .constants import BITBUCKET_IP_RANGES , BITBUCKET_IPS
23
+ from sentry .integrations .services .integration .service import integration_service
20
24
from sentry .integrations .source_code_management .webhook import SCMWebhook
21
25
from sentry .integrations .utils .metrics import IntegrationWebhookEvent , IntegrationWebhookEventType
22
26
from sentry .models .commit import Commit
31
35
PROVIDER_NAME = "integrations:bitbucket"
32
36
33
37
38
+ def is_valid_signature (body : bytes , secret : str , signature : str ) -> bool :
39
+ hash_object = hmac .new (
40
+ secret .encode ("utf-8" ),
41
+ msg = body ,
42
+ digestmod = hashlib .sha256 ,
43
+ )
44
+ expected_signature = hash_object .hexdigest ()
45
+
46
+ if not hmac .compare_digest (expected_signature , signature ):
47
+ logger .info (
48
+ "%s.webhook.invalid-signature" ,
49
+ PROVIDER_NAME ,
50
+ extra = {"expected" : expected_signature , "given" : signature },
51
+ )
52
+ return False
53
+ return True
54
+
55
+
56
+ class WebhookMissingSignatureException (SentryAPIException ):
57
+ status_code = 400
58
+ code = f"{ PROVIDER_NAME } .webhook.missing-signature"
59
+ message = "Missing webhook signature"
60
+
61
+
62
+ class WebhookUnsupportedSignatureMethodException (SentryAPIException ):
63
+ status_code = 400
64
+ code = f"{ PROVIDER_NAME } .webhook.unsupported-signature-method"
65
+ message = "Signature method is not supported"
66
+
67
+
68
+ class WebhookInvalidSignatureException (SentryAPIException ):
69
+ status_code = 400
70
+ code = f"{ PROVIDER_NAME } .webhook.invalid-signature"
71
+ message = "Webhook signature is invalid"
72
+
73
+
34
74
class BitbucketWebhook (SCMWebhook , ABC ):
35
75
@property
36
76
def provider (self ) -> str :
@@ -74,6 +114,8 @@ def event_type(self) -> IntegrationWebhookEventType:
74
114
def __call__ (self , event : Mapping [str , Any ], ** kwargs ) -> None :
75
115
authors = {}
76
116
117
+ if not (request := kwargs .get ("request" )):
118
+ raise ValueError ("Missing request" )
77
119
if not (organization := kwargs .get ("organization" )):
78
120
raise ValueError ("Missing organization" )
79
121
@@ -86,6 +128,20 @@ def __call__(self, event: Mapping[str, Any], **kwargs) -> None:
86
128
except Repository .DoesNotExist :
87
129
raise Http404 ()
88
130
131
+ integration = integration_service .get_integration (integration_id = repo .integration_id )
132
+ if integration and "webhook_secret" in integration .metadata :
133
+ secret = integration .metadata ["webhook_secret" ]
134
+ try :
135
+ method , signature = request .META ["HTTP_X_HUB_SIGNATURE" ].split ("=" , 1 )
136
+ except (IndexError , KeyError , ValueError ):
137
+ raise WebhookMissingSignatureException ()
138
+
139
+ if method != "sha256" :
140
+ raise WebhookUnsupportedSignatureMethodException ()
141
+
142
+ if not is_valid_signature (request .body , secret , signature ):
143
+ raise WebhookInvalidSignatureException ()
144
+
89
145
# while we're here, make sure repo data is up to date
90
146
self .update_repo_data (repo , event )
91
147
@@ -206,6 +262,6 @@ def post(self, request: HttpRequest, organization_id: int) -> HttpResponse:
206
262
domain = IntegrationDomain .SOURCE_CODE_MANAGEMENT ,
207
263
provider_key = event_handler .provider ,
208
264
).capture ():
209
- event_handler (event , organization = organization )
265
+ event_handler (event , request = request , organization = organization )
210
266
211
267
return HttpResponse (status = 204 )
0 commit comments