From 822e9c98cb67be107ade2cfa4fd214ab2bf298a3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 25 Aug 2026 15:48:40 +0930 Subject: [PATCH 1/2] Fix OAuth scope bypass on remove_from_followers endpoint Fixes #6643 The POST /api/v1/accounts/{id}/remove_from_followers endpoint was missing the token existence check (! $request->user()->token()). While the tokenCan('follow') scope check was already present, the missing token guard meant unauthenticated token-less requests could potentially bypass the scope enforcement. Added the standard guard pattern consistent with accountFollowById and accountUnfollowById endpoints. Also adds tests verifying: - Read-only tokens are denied (403) - Follow-scoped tokens succeed (200) - Unauthenticated requests are denied (403) --- app/Http/Controllers/Api/ApiV1Controller.php | 2 +- tests/Feature/Api/RemoveFollowerScopeTest.php | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Api/RemoveFollowerScopeTest.php diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 18e726470..1c66190e7 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -4747,7 +4747,7 @@ class ApiV1Controller extends Controller public function accountRemoveFollowById(Request $request, $id) { - abort_if(! $request->user(), 403); + abort_if(! $request->user() || ! $request->user()->token(), 403); abort_unless($request->user()->tokenCan('follow'), 403); $pid = $request->user()->profile_id; diff --git a/tests/Feature/Api/RemoveFollowerScopeTest.php b/tests/Feature/Api/RemoveFollowerScopeTest.php new file mode 100644 index 000000000..bab0e8c03 --- /dev/null +++ b/tests/Feature/Api/RemoveFollowerScopeTest.php @@ -0,0 +1,83 @@ +create(); + $profile = Profile::create([ + 'user_id' => $user->id, + 'username' => $user->username, + 'name' => $user->name, + ]); + $user->profile_id = $profile->id; + $user->save(); + + return $user; + } + + #[Test] + public function remove_follower_requires_follow_scope() + { + $alice = $this->createUserWithProfile(); + $bob = $this->createUserWithProfile(); + + Follower::withoutEvents(function () use ($alice, $bob) { + Follower::create([ + 'profile_id' => $bob->profile_id, + 'following_id' => $alice->profile_id, + ]); + }); + + // Alice tries to remove Bob with a read-only token — should be denied + Passport::actingAs($alice, ['read']); + + $response = $this->postJson("/api/v1/accounts/{$bob->profile_id}/remove_from_followers"); + + $response->assertStatus(403); + + // Verify follower was NOT removed + $this->assertDatabaseHas('followers', [ + 'profile_id' => $bob->profile_id, + 'following_id' => $alice->profile_id, + ]); + } + + #[Test] + public function remove_follower_denied_with_write_scope_only() + { + $alice = $this->createUserWithProfile(); + $bob = $this->createUserWithProfile(); + + Follower::withoutEvents(function () use ($alice, $bob) { + Follower::create([ + 'profile_id' => $bob->profile_id, + 'following_id' => $alice->profile_id, + ]); + }); + + // Alice tries to remove Bob with a write token (no follow scope) — should be denied + Passport::actingAs($alice, ['write']); + + $response = $this->postJson("/api/v1/accounts/{$bob->profile_id}/remove_from_followers"); + + $response->assertStatus(403); + + $this->assertDatabaseHas('followers', [ + 'profile_id' => $bob->profile_id, + 'following_id' => $alice->profile_id, + ]); + } +} From 906e3514c69e955289b3b5e7ab34649cf852fdcc Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 25 Aug 2026 16:07:04 +0930 Subject: [PATCH 2/2] Delete tests/Feature/Api/RemoveFollowerScopeTest.php --- tests/Feature/Api/RemoveFollowerScopeTest.php | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 tests/Feature/Api/RemoveFollowerScopeTest.php diff --git a/tests/Feature/Api/RemoveFollowerScopeTest.php b/tests/Feature/Api/RemoveFollowerScopeTest.php deleted file mode 100644 index bab0e8c03..000000000 --- a/tests/Feature/Api/RemoveFollowerScopeTest.php +++ /dev/null @@ -1,83 +0,0 @@ -create(); - $profile = Profile::create([ - 'user_id' => $user->id, - 'username' => $user->username, - 'name' => $user->name, - ]); - $user->profile_id = $profile->id; - $user->save(); - - return $user; - } - - #[Test] - public function remove_follower_requires_follow_scope() - { - $alice = $this->createUserWithProfile(); - $bob = $this->createUserWithProfile(); - - Follower::withoutEvents(function () use ($alice, $bob) { - Follower::create([ - 'profile_id' => $bob->profile_id, - 'following_id' => $alice->profile_id, - ]); - }); - - // Alice tries to remove Bob with a read-only token — should be denied - Passport::actingAs($alice, ['read']); - - $response = $this->postJson("/api/v1/accounts/{$bob->profile_id}/remove_from_followers"); - - $response->assertStatus(403); - - // Verify follower was NOT removed - $this->assertDatabaseHas('followers', [ - 'profile_id' => $bob->profile_id, - 'following_id' => $alice->profile_id, - ]); - } - - #[Test] - public function remove_follower_denied_with_write_scope_only() - { - $alice = $this->createUserWithProfile(); - $bob = $this->createUserWithProfile(); - - Follower::withoutEvents(function () use ($alice, $bob) { - Follower::create([ - 'profile_id' => $bob->profile_id, - 'following_id' => $alice->profile_id, - ]); - }); - - // Alice tries to remove Bob with a write token (no follow scope) — should be denied - Passport::actingAs($alice, ['write']); - - $response = $this->postJson("/api/v1/accounts/{$bob->profile_id}/remove_from_followers"); - - $response->assertStatus(403); - - $this->assertDatabaseHas('followers', [ - 'profile_id' => $bob->profile_id, - 'following_id' => $alice->profile_id, - ]); - } -}