23
23
*/
24
24
package com .cloudbees .jenkins .plugins .bitbucket .hooks ;
25
25
26
+ import com .cloudbees .jenkins .plugins .bitbucket .endpoints .AbstractBitbucketEndpoint ;
27
+ import com .cloudbees .jenkins .plugins .bitbucket .endpoints .BitbucketCloudEndpoint ;
28
+ import com .cloudbees .jenkins .plugins .bitbucket .endpoints .BitbucketEndpointConfiguration ;
29
+ import com .cloudbees .jenkins .plugins .bitbucket .impl .util .BitbucketCredentials ;
30
+ import com .cloudbees .plugins .credentials .common .StandardCredentials ;
26
31
import hudson .Extension ;
27
32
import hudson .model .UnprotectedRootAction ;
28
33
import hudson .security .csrf .CrumbExclusion ;
29
34
import hudson .util .HttpResponses ;
35
+ import hudson .util .Secret ;
30
36
import jakarta .servlet .FilterChain ;
31
37
import jakarta .servlet .ServletException ;
32
38
import jakarta .servlet .http .HttpServletRequest ;
35
41
import java .nio .charset .StandardCharsets ;
36
42
import java .util .logging .Level ;
37
43
import java .util .logging .Logger ;
44
+ import javax .crypto .Mac ;
45
+ import jenkins .model .Jenkins ;
38
46
import jenkins .scm .api .SCMEvent ;
47
+ import org .apache .commons .codec .digest .HmacUtils ;
39
48
import org .apache .commons .io .IOUtils ;
49
+ import org .apache .commons .lang .StringUtils ;
50
+ import org .jenkinsci .plugins .plaincredentials .StringCredentials ;
40
51
import org .kohsuke .stapler .HttpResponse ;
41
52
import org .kohsuke .stapler .StaplerRequest2 ;
42
53
@@ -78,6 +89,7 @@ public String getUrlName() {
78
89
public HttpResponse doNotify (StaplerRequest2 req ) throws IOException {
79
90
String origin = SCMEvent .originOf (req );
80
91
String body = IOUtils .toString (req .getInputStream (), StandardCharsets .UTF_8 );
92
+
81
93
String eventKey = req .getHeader ("X-Event-Key" );
82
94
if (eventKey == null ) {
83
95
return HttpResponses .error (HttpServletResponse .SC_BAD_REQUEST , "X-Event-Key HTTP header not found" );
@@ -89,20 +101,58 @@ public HttpResponse doNotify(StaplerRequest2 req) throws IOException {
89
101
}
90
102
91
103
String bitbucketKey = req .getHeader ("X-Bitbucket-Type" );
92
- String serverUrl = req .getParameter ("server_url" );
104
+ String serverURL = req .getParameter ("server_url" );
105
+
93
106
BitbucketType instanceType = null ;
94
107
if (bitbucketKey != null ) {
95
108
instanceType = BitbucketType .fromString (bitbucketKey );
96
109
}
97
- if (instanceType == null && serverUrl != null ) {
110
+ if (instanceType == null && serverURL != null ) {
98
111
LOGGER .log (Level .FINE , "server_url request parameter found. Bitbucket Native Server webhook incoming." );
99
112
instanceType = BitbucketType .SERVER ;
100
113
} else {
101
114
LOGGER .log (Level .FINE , "X-Bitbucket-Type header / server_url request parameter not found. Bitbucket Cloud webhook incoming." );
115
+ instanceType = BitbucketType .CLOUD ;
116
+ serverURL = BitbucketCloudEndpoint .SERVER_URL ;
117
+ }
118
+
119
+ String signatureHeader = req .getParameter ("X-Hub-Signature" );
120
+ if (signatureHeader != null ) {
121
+ // TODO lookup the serverURL hostname from the http request instead trust in the payload, consider also proxy headers
122
+ AbstractBitbucketEndpoint endpoint = BitbucketEndpointConfiguration .get ()
123
+ .findEndpoint (serverURL )
124
+ .orElse (null );
125
+ if (endpoint == null ) {
126
+ LOGGER .log (Level .INFO , "No bitbucket endpoint found for {} to verify the signature of incoming webhook." , serverURL );
127
+ } else if (endpoint .isManageHooks ()) {
128
+ String hmacCredentialsId = endpoint .getHMACCredentialsId ();
129
+
130
+ String algorithm = StringUtils .substringBefore (signatureHeader , "=" );
131
+ String signature = StringUtils .substringAfter (signatureHeader , "=" );
132
+ String key ;
133
+ StringCredentials hmacCredentials = BitbucketCredentials .lookupCredentials (serverURL , Jenkins .get (), hmacCredentialsId , StringCredentials .class );
134
+ if (hmacCredentials != null ) {
135
+ key = Secret .toString (hmacCredentials .getSecret ());
136
+ HmacUtils util ;
137
+ try {
138
+ util = new HmacUtils (algorithm , key .getBytes ());
139
+ } catch (IllegalArgumentException e ) {
140
+ return HttpResponses .error (HttpServletResponse .SC_BAD_REQUEST , "Signature method not supported: " + signature );
141
+ }
142
+ String mac = util .hmacHex (body );
143
+ if (!mac .equals (signature )) {
144
+ return HttpResponses .error (HttpServletResponse .SC_FORBIDDEN , "Signature does not match" );
145
+ }
146
+ } else {
147
+ LOGGER .log (Level .WARNING , "No credentials {} found to verify the signature of incoming webhook." , hmacCredentialsId );
148
+ }
149
+ } else {
150
+ LOGGER .log (Level .FINE , "Signature of incoming webhook not configurted for endpoint {}." , serverURL );
151
+ }
102
152
}
103
153
104
154
HookProcessor hookProcessor = getHookProcessor (type );
105
- hookProcessor .process (type , body , instanceType , origin , serverUrl );
155
+ hookProcessor .process (type , body , instanceType , origin , serverURL );
106
156
return HttpResponses .ok ();
107
157
}
108
158
0 commit comments