Laravel: Disabling Pagination Causes 403 on Policy-Protected Collection Endpoints
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 72/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- laravel, php
- 領域
- api, authorization, backend
調査の方向性
CollectionProvider.php と ResourceAccessChecker.php から始め、次にアクセス拒否パスにある AccessCheckerProvider.php を調査します。viewAny() が true を返す policy を使用して、ページネーションを有効にした場合と無効にした場合の 2 つの collection リクエストを再現します。pagination が無効であることだけを理由に、policy で保護された collection エンドポイントが 403 を返さなくなり、pagination=false と pagination=0 の両方に対するリグレッションカバレッジが追加されていれば完了です。
索引モデルが issue の本文から書いたものです。
説明
API Platform version(s) affected: 4.3.17 (api-platform/laravel)
Description
Any GetCollection operation that has a Policy/security expression (resolved via a Laravel Policy's viewAny() method, e.g. through the standard Gate::getPolicyFor($model) naming-convention resolution) unconditionally returns 403 Access Denied when the request disables client-side pagination (?pagination=false or ?pagination=0), regardless of authentication state or the policy's actual logic — even when viewAny() unconditionally returns true.
Root cause: ApiPlatform\Laravel\Security\ResourceAccessChecker::isGranted() special-cases the Paginator wrapper class when deciding what object to pass to Gate::allows():
public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool
{
$object = $extraVariables['object'] ?? null;
return Gate::allows(
$expression,
($object instanceof Paginator || null === $object) ? $resourceClass : $object
);
}
When pagination is enabled, ApiPlatform\Laravel\Eloquent\State\CollectionProvider::provide() returns an ApiPlatform\Laravel\Eloquent\Paginator instance, so $object instanceof Paginator is true, and Gate::allows($expression, $resourceClass) is called with the resource class string — which correctly resolves to the model's Policy class and calls viewAny().
When pagination is disabled, the same provider instead returns a plain Illuminate\Database\Eloquent\Collection (see CollectionProvider::provide(), the false === $this->pagination->isEnabled(...) branch: return $query->get();). This is not a Paginator, so ResourceAccessChecker passes the Collection object itself to Gate::allows($expression, $collection). Laravel's Gate then tries to resolve a policy based on get_class($collection) (Illuminate\Database\Eloquent\Collection), finds no policy registered for that class, and — absent any other applicable ability/Gate::before() override — denies the request by default.
The result: disabling pagination on any policy-protected collection endpoint always 403s, independent of the actual authorization rule.
How to reproduce
Any Eloquent resource with a Policy providing viewAny():
class PaymentMethodPolicy
{
public function viewAny(?RemoteUser $user): bool
{
return true; // unconditionally public
}
}
GET /api/payment_methods
→ 200 OK (Gate::allows('viewAny', PaymentMethod::class) — resolves the policy correctly)
GET /api/payment_methods?pagination=false
→ 403 {"detail":"Access Denied."}
(reproduces identically authenticated or unauthenticated)
GET /api/payment_methods?pagination=0
→ 403 (same)
Possible Solution
ResourceAccessChecker::isGranted() should pass the resource class string whenever $object is a bare collection of resources (i.e., not a single instance of the resource itself) rather than only special-casing Paginator. For example, also treat PartialPaginator and plain Illuminate\Support\Collection/Illuminate\Database\Eloquent\Collection instances the same way Paginator is treated:
$isCollectionLike = $object instanceof Paginator
|| $object instanceof PartialPaginator
|| $object instanceof \Illuminate\Support\Collection;
return Gate::allows(
$expression,
($isCollectionLike || null === $object) ? $resourceClass : $object
);
Additional Context
CollectionProvider:vendor/api-platform/laravel/Eloquent/State/CollectionProvider.phpResourceAccessChecker:vendor/api-platform/laravel/Security/ResourceAccessChecker.phpAccessCheckerProvider(throw site):vendor/api-platform/laravel/State/AccessCheckerProvider.php- This was found while debugging a checkout page that couldn't load any payment methods; it was initially conflated with an unrelated
BooleanFiltervalidation bug (seedocs/bug-reports/api-platform-boolean-filter-validation.md) because both bugs happened to be triggered by parameters on the same request (?enabled=true&pagination=false) and both produce failure responses that look superficially similar during triage (422 vs 403) unless isolated parameter-by-parameter. - Workaround applied in our app: stopped disabling client-side pagination on this endpoint (removed
pagination: falsefrom the frontend query) — the resource's default page size (20) is more than sufficient for a payment-methods list, so no functional loss.
- 主要言語
- PHP
- スター
- 2.6k
- フォーク
- 982
- 平均マージ
- 1日 16時間
- マージ済み PR(30日)
- 59
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
api-platform/core のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
api-platform/core#8573 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
api-platform/core#8571 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
api-platform/core#8564 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
api-platform/core#8495 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
api-platform/core#8471 ·
api-platform/core の issue をすべて見る
似ている issue
-
Bug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
Automattic/safe-publish#594 ·
-
HttpClient
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
symfony/symfony-docs#23092 ·
-
sync-en
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
-
sync-en
難易度 1/5 1時間未満 初心者へのやさしさ 95/100
-
Перевод устарел
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100