diff --git a/app/Models/AuditEvent.php b/app/Models/AuditEvent.php index 45c8b632fa..b966e55706 100644 --- a/app/Models/AuditEvent.php +++ b/app/Models/AuditEvent.php @@ -269,6 +269,10 @@ class AuditEvent extends Model $value = (array) $value; } + if (is_string($value)) { + return self::stripUrlCredentials($value); + } + if (! is_array($value)) { return $value; } @@ -280,6 +284,14 @@ class AuditEvent extends Model ->all(); } + /** + * Removes the user info of URLs (https://user:token@host), so the audit keeps the URL without credentials. + */ + private static function stripUrlCredentials(string $value): string + { + return preg_replace('#\b([a-z][a-z0-9+.-]*://)[^/?\#\s]+@#i', '$1', $value) ?? $value; + } + private static function isSensitiveKey(string $key): bool { if (preg_match('/_(id|uuid|name)$/i', $key)) { diff --git a/tests/Feature/AuditGitCredentialRedactionTest.php b/tests/Feature/AuditGitCredentialRedactionTest.php new file mode 100644 index 0000000000..a2c654387b --- /dev/null +++ b/tests/Feature/AuditGitCredentialRedactionTest.php @@ -0,0 +1,58 @@ +withoutDefer(); + + InstanceSettings::forceCreate(['id' => 0]); + Once::flush(); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +test('audit changes do not store credentials embedded in the git repository url', function () { + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'git_repository' => 'https://oauth2:glpat-old@token@gitlab.com/org/old.git', + ]); + AuditEvent::query()->delete(); + + $application->update(['git_repository' => 'https://oauth2:glpat-new-token@gitlab.com/org/repo.git']); + + $event = AuditEvent::query()->sole(); + + expect($event->changes['git_repository'])->toBe([ + 'old' => 'https://gitlab.com/org/old.git', + 'new' => 'https://gitlab.com/org/repo.git', + ]) + ->and(json_encode($event->changes))->not->toContain('glpat') + ->and(json_encode($event->metadata))->not->toContain('glpat'); +}); + +test('audit changes keep git repository urls without credentials unchanged', function () { + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create(['environment_id' => $environment->id]); + AuditEvent::query()->delete(); + + $application->update(['git_repository' => 'git@github.com:org/repo.git']); + + expect(AuditEvent::query()->sole()->changes['git_repository']['new'])->toBe('git@github.com:org/repo.git'); +});