Vulnerability Description
The GitLab webhook endpoint uses a non-constant-time string comparison operator (!==) to validate the webhook secret token. This implementation is vulnerable to timing attacks, which could allow an attacker to gradually discover the secret token by measuring response time differences.
Technical Details
Vulnerable Code Location: app/Http/Controllers/Webhook/Gitlab.php:103
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
if ($webhook_secret !== $x_gitlab_token) {
// Returns "Invalid signature"
}
PHP's standard string comparison operator (!==) performs a byte-by-byte comparison and returns as soon as a mismatch is found. This creates a timing side-channel where:
- Tokens with more matching prefix characters take longer to compare
- An attacker can statistically determine each character of the token by measuring response times
Comparison with Secure Implementation
The GitHub webhook handler in the same codebase correctly uses hash_equals():
Secure Code: app/Http/Controllers/Webhook/Github.php:83
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
// Returns "Invalid signature"
}
The Bitbucket webhook handler also uses the secure approach:
Secure Code: app/Http/Controllers/Webhook/Bitbucket.php:64
$payloadHash = hash_hmac($algo, $payload, $webhook_secret);
if (! hash_equals($hash, $payloadHash) && ! isDev()) {
// Returns "Invalid signature"
}
Attack Scenario
Prerequisites
- Attacker knows the GitLab repository name associated with a Coolify application
- Attacker has network access to the Coolify webhook endpoint
- Network conditions are stable enough to measure timing differences (typically < 50ms jitter)
Attack Steps
-
Reconnaissance: Attacker identifies an application using GitLab webhooks (e.g., through public deployment logs or error messages)
-
Baseline Measurement: Attacker sends multiple requests with random tokens to establish baseline response times
-
Character Enumeration: For each position in the token:
- Send requests with all possible characters at that position
- Measure response times for each request
- The character producing the longest response time is likely correct
-
Token Recovery: After recovering all characters, attacker possesses the valid webhook token
-
Exploitation: With the valid token, attacker can:
- Trigger unauthorized deployments
- Inject malicious code through fake push events
- Disrupt CI/CD pipelines
Proof of Concept
A working PoC script is available that demonstrates this attack:
# timing_attack.py (excerpt)
def measure_request(self, token: str, samples: int = 10) -> float:
times = []
for _ in range(samples):
start = time.perf_counter()
response = self.session.post(
self.webhook_url,
data=payload,
headers={"X-Gitlab-Token": token}
)
end = time.perf_counter()
times.append(end - start)
return statistics.median(times)
Impact Assessment
Confidentiality
- HIGH: Webhook secrets can be recovered, exposing authentication credentials
Integrity
- HIGH: Attacker can trigger unauthorized deployments and inject malicious code
Availability
- MEDIUM: Attacker could disrupt deployments or trigger excessive builds
CVSS 3.1 Score Calculation
Vector: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N
Score: 5.3 (Medium)
- Attack Vector: Network (AV:N)
- Attack Complexity: High (AC:H) - requires many requests and stable network
- Privileges Required: None (PR:N)
- User Interaction: None (UI:N)
- Scope: Unchanged (S:U)
- Confidentiality: High (C:H) - token disclosure
- Integrity: Low (I:L) - unauthorized deployments possible
- Availability: None (A:N)
Recommended Fix
Immediate Fix
Replace the vulnerable comparison with hash_equals():
// File: app/Http/Controllers/Webhook/Gitlab.php
// Line: 103
// Before (vulnerable):
if ($webhook_secret !== $x_gitlab_token) {
// After (secure):
if (!hash_equals($webhook_secret ?? '', $x_gitlab_token ?? '')) {
Complete Patch
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -100,7 +100,7 @@ class Gitlab extends Controller
}
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
- if ($webhook_secret !== $x_gitlab_token) {
+ if (!hash_equals($webhook_secret ?? '', $x_gitlab_token ?? '')) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
Why This Fix Works
hash_equals() is specifically designed for comparing secrets:
- Performs comparison in constant time regardless of where strings differ
- Prevents timing side-channels
- Available in PHP 5.6.0+ (built-in function)
References
Vulnerability Description
The GitLab webhook endpoint uses a non-constant-time string comparison operator (
!==) to validate the webhook secret token. This implementation is vulnerable to timing attacks, which could allow an attacker to gradually discover the secret token by measuring response time differences.Technical Details
Vulnerable Code Location:
app/Http/Controllers/Webhook/Gitlab.php:103PHP's standard string comparison operator (
!==) performs a byte-by-byte comparison and returns as soon as a mismatch is found. This creates a timing side-channel where:Comparison with Secure Implementation
The GitHub webhook handler in the same codebase correctly uses
hash_equals():Secure Code:
app/Http/Controllers/Webhook/Github.php:83The Bitbucket webhook handler also uses the secure approach:
Secure Code:
app/Http/Controllers/Webhook/Bitbucket.php:64Attack Scenario
Prerequisites
Attack Steps
Reconnaissance: Attacker identifies an application using GitLab webhooks (e.g., through public deployment logs or error messages)
Baseline Measurement: Attacker sends multiple requests with random tokens to establish baseline response times
Character Enumeration: For each position in the token:
Token Recovery: After recovering all characters, attacker possesses the valid webhook token
Exploitation: With the valid token, attacker can:
Proof of Concept
A working PoC script is available that demonstrates this attack:
Impact Assessment
Confidentiality
Integrity
Availability
CVSS 3.1 Score Calculation
Recommended Fix
Immediate Fix
Replace the vulnerable comparison with
hash_equals():Complete Patch
Why This Fix Works
hash_equals()is specifically designed for comparing secrets:References