From eed50d67811378e030b917bd7c060087de6c3281 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:18:29 +0200 Subject: [PATCH] fix(github): check the private key before generating app tokens Co-Authored-By: Claude Opus 5.5 --- bootstrap/helpers/github.php | 13 ++++++-- .../Application/GithubSourceChangeTest.php | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index b5936f2312..d66ffd88b8 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -166,11 +166,20 @@ function assertGithubClockInSync(string $apiUrl): void } } +function githubAppPrivateKey(GithubApp $source): ?PrivateKey +{ + $privateKey = $source->privateKey; + + return $privateKey?->team_id === $source->team_id ? $privateKey : null; +} + function generateGithubToken(GithubApp $source, string $type) { + $privateKey = githubAppPrivateKey($source) ?? throw new RuntimeException('Private key not found for this GitHub App.'); + assertGithubClockInSync($source->api_url); - $signingKey = InMemory::plainText($source->privateKey->private_key); + $signingKey = InMemory::plainText($privateKey->private_key); $algorithm = new Sha256; $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); $now = CarbonImmutable::now()->setTimezone('UTC'); @@ -279,7 +288,7 @@ function syncGithubAppName(GithubApp $source, bool $throw = false): ?string return null; } - $privateKey = $source->privateKey ?: PrivateKey::find($source->private_key_id); + $privateKey = githubAppPrivateKey($source); if (! $privateKey) { return null; diff --git a/tests/Feature/Application/GithubSourceChangeTest.php b/tests/Feature/Application/GithubSourceChangeTest.php index b179fa121b..44baaf8fb6 100644 --- a/tests/Feature/Application/GithubSourceChangeTest.php +++ b/tests/Feature/Application/GithubSourceChangeTest.php @@ -278,6 +278,37 @@ describe('GitHub Source Change Component', function () { expect($githubApp->refresh()->private_key_id)->toBe($ownKey->id); })->with(['submit', 'instantSave']); + test('github app tokens only use a key from the source team', function () { + Http::fake(); + + $otherTeam = Team::factory()->create(); + $foreignKey = PrivateKey::create([ + 'name' => 'other-team-key', + 'private_key' => validPrivateKey(), + 'team_id' => $otherTeam->id, + ]); + + $githubApp = GithubApp::create([ + 'name' => 'Test GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'app_id' => 12345, + 'installation_id' => 67890, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + ]); + GithubApp::query()->whereKey($githubApp->id)->update(['private_key_id' => $foreignKey->id]); + $githubApp->refresh(); + + expect(fn () => generateGithubJwt($githubApp))->toThrow(RuntimeException::class) + ->and(syncGithubAppName($githubApp))->toBeNull() + ->and($foreignKey->refresh()->name)->toBe('other-team-key'); + + Http::assertNothingSent(); + }); + test('ghe.com installation path encodes the organization segment', function () { $githubApp = new GithubApp; $githubApp->forceFill([