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
+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) {