From 822e9c98cb67be107ade2cfa4fd214ab2bf298a3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 25 Aug 2026 15:48:40 +0930 Subject: [PATCH] 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, + ]); + } +}