fix(security): encrypt GitLab webhook token and mask input

Webhook secret was stored and shown as plaintext. Use a password field,
encrypt at rest (with legacy plaintext read support), and look up tokens
via findByWebhookToken so encrypted values still authenticate webhooks.
This commit is contained in:
Andras Bacsai
2026-07-21 21:21:04 +02:00
parent 94c3129ad1
commit 43919ef4e0
5 changed files with 118 additions and 4 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ class Gitlab extends Controller
], 401);
}
$gitlab_app = GitlabApp::where('webhook_token', $x_gitlab_token)->first();
$gitlab_app = GitlabApp::findByWebhookToken($x_gitlab_token);
if (! $gitlab_app) {
auditLogWebhookFailure('gitlab', 'invalid_token', [
'event' => $object_kind,
+3 -1
View File
@@ -237,7 +237,9 @@ class Change extends Component
if (! empty($this->clientSecretInput)) {
$this->gitlab_app->client_secret = $this->clientSecretInput;
}
$this->gitlab_app->webhook_token = $this->webhookToken;
if (! empty($this->webhookToken)) {
$this->gitlab_app->webhook_token = $this->webhookToken;
}
$this->gitlab_app->group_name = $this->groupName;
$this->gitlab_app->is_system_wide = $this->isSystemWide;
$this->gitlab_app->private_key_id = $this->privateKeyId;
+45
View File
@@ -2,6 +2,10 @@
namespace App\Models;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\Crypt;
class GitlabApp extends BaseModel
{
protected $fillable = [
@@ -49,6 +53,47 @@ class GitlabApp extends BaseModel
];
}
/**
* Encrypt webhook tokens at rest. Supports legacy plaintext values until they are re-saved.
* Not a standard encrypted cast: webhooks look up by token value (see findByWebhookToken).
*/
protected function webhookToken(): Attribute
{
return Attribute::make(
get: function (?string $value): ?string {
if ($value === null || $value === '') {
return $value;
}
try {
return Crypt::decryptString($value);
} catch (DecryptException) {
// Legacy rows stored the token in plaintext.
return $value;
}
},
set: function (?string $value): ?string {
if ($value === null || $value === '') {
return $value;
}
return Crypt::encryptString($value);
},
);
}
public static function findByWebhookToken(string $token): ?self
{
if ($token === '') {
return null;
}
// Encrypted values cannot be matched with a SQL equality; sources are few per instance.
return static::query()->get()->first(
fn (self $app): bool => filled($app->webhook_token) && hash_equals((string) $app->webhook_token, $token)
);
}
protected static function booted(): void
{
static::deleting(function (GitlabApp $gitlabApp) {