1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

return $this->redirect() を setResult() にしたところ認可エラーが顕在化 - CakePHP 5.2 Deprecation

1
Posted at

サマリ

  • CakePHP 5.2 で「イベントリスナーからの値 return」が deprecated になりました
  • return $this->redirect(...) の deprecation を $event->setResult($this->redirect(...)); return; する形で修正したところ、 500 エラー did not apply any authorization checks になってしまっていました
  • parent::beforeFilter($event); していたところ、return が伝播せず、結果として想定外に actionが走っていました。このとき、action内で authorize()(認可)が実行され、redirect() の副作用でリダイレクトも成立していました
  • setResult() にしたところ、parent::beforeFilter($event); していても、actionが呼ばれなくなります。結果 cakephp/authorization 利用時はactionが実行されず認可チェックも走らず、エラーとなっていました。
  • 対処は、リダイレクトの手前で $this->Authorization->skipAuthorization() を呼ぶこと。

修正後のコード:

public function beforeFilter(\Cake\Event\EventInterface $event): void
{
    parent::beforeFilter($event);

    if ($someCondition) {
        // 短絡するとactionが実行されない=認可チェックも走らないので明示的にスキップ
        $this->Authorization->skipAuthorization();
        $event->setResult($this->redirect(['_name' => 'login']));

        return;
    }
}

きっかけ: CakePHP 5.2 の deprecation(イベントリスナーからの値 return)

beforeFilter(イベントリスナー)で、条件に応じて別ページへ飛ばすリダイレクトを書いていました。

public function beforeFilter(EventInterface $event)
{
    // 中略

    return $this->redirect(['_name' => 'login']);
}

ところが CakePHP 5.2 で「イベントリスナーから値を return する」のが deprecated になり、次の警告が出ます。

Since 5.2.0: Returning a value from event listeners is deprecated. Use $event->setResult() instead

公式の推奨どおり $event->setResult() に直したところ—— 500エラー(認可エラー)が発生しました。

// 500 エラー(認可エラー)が発生
public function beforeFilter(EventInterface $event): void
{
    // 中略

    $event->setResult($this->redirect(['_name' => 'login']));

    return;
}

なぜ修正前は動作していたのか

return $this->redirect(['_name' => 'login']); でもイベントは止まるはずなので、修正後のコードと同じ状況になると思いましたが、違いました。

別の原因で、結果的に action の実行はキャンセルされず、その中で認可を通る結果となっており、問題を抱えたまま動作していたという結論でした。

このプロジェクトでは、該当のフロー制御は親 AppController::beforeFilter() にて実装しており、各コントローラは下記のように記載していました(公式ドキュメントの通り)。

// 公式 5.x doc(Controllers)より
public function beforeFilter(EventInterface $event): void
{
    parent::beforeFilter($event);
}

Remember to call AppController's callbacks within child controller callbacks for best results

(日本語)最良の結果を得るために、子コントローラーのコールバック中で AppController のコールバックを呼ぶのを忘れないでください。

公式ドキュメントには上記のように記載されています。parent::beforeFilter($event); であって return parent::beforeFilter($event); ではありません。結果として、親メソッドの戻り値はどこにもリターンされない結果となります。

つまり、たとえ親コントローラーで AppController::beforeFilter()return $this->redirect(...) で Response を返しても、

  • 子の parent::beforeFilter($event);(文)がリターン値を扱わず、

イベントリスナーとしての戻り値は null となります。そのため、action がそのまま実行されます

では、なぜ修正前のコードは動いていたのかですが、

  1. redirect()$this->response を 302(+ Location)に設定する。
  2. return $this->redirect(...) で値を return しているにも関わらず、AppController::beforeFilter() 側の実装のためactionの実行がキャンセルされず、その中で $this->Authorization->authorize(...) を実行していました。

1 によってリダイレクトは成立し、2 によって認可も満たされていました。「リダイレクトしているのにactionも走る」
意図せず action が実施されるので、アプリの不整合にもつながりえますし、セキュリティ的にも問題がある構成となっていました。

5.2 の deprecation 警告に従って setResult に直したことで、この「actionも走っていた」構成が発覚しました。


なぜ setResult だと 500 になるのか

setResult() は return で値を返すことをやめ、$event オブジェクトを書き換えます。そのため parent::beforeFilter($event); などによらず、イベントの結果は確実に伝播します。
結果として、actionの実行がキャンセルされます

公式ドキュメント Controllers — Using Redirects in Controller Events の掲載コード:

// 公式 doc より
public function beforeFilter(EventInterface $event): void
{
    if ($this->request->getParam('prefix') !== 'Admin') {
        $event->setResult($this->redirect('/'));

        return;
    }
}

なお、この setResult を使うリダイレクトの節(Using Redirects in Controller Events)は英語版の Controllers ドキュメントにのみ記載されており、日本語版には対応する記載がありませんでした(2026/6時点)。

actionが実行されないため、その中で呼んでいた $this->Authorization->authorize(...) も呼ばれません。すると cakephp/authorization 利用時に次の例外が出ます。

Authorization\Exception\AuthorizationRequiredException:
The request to `/dashboard` did not apply any authorization checks.

AuthorizationMiddleware は、リクエスト処理後に「認可チェックが一度でも行われたか」を確認し、行われていなければ例外を投げます。

# Middleware/AuthorizationMiddleware.php より抜粋
if ($this->getConfig('requireAuthorizationCheck') && !$service->authorizationChecked()) {
    throw new AuthorizationRequiredException(['url' => $request->getRequestTarget()]);
}

このため AuthorizationRequiredException となり、500 エラーとなります。

これは setResult 固有の問題ではなく、「リダイレクトでactionが実行されない、そのため、認可チェックが行われない」という問題です。
同じ問題は、FriendsOfCake/Search の PRG パターンでも報告されていました(cakephp/authorization #128)。


対処方法: redirect 前に skipAuthorization を呼ぶ

上記公式 issue で示されているように、beforeFilter$this->Authorization->skipAuthorization() を呼ぶことで解決します。

public function beforeFilter(EventInterface $event): void
{
    if ($this->request->getParam('prefix') !== 'Admin') {
        // この分岐は action が実行されない=認可チェックも行われないため、明示的にチェックをスキップする
        $this->Authorization->skipAuthorization();
        $event->setResult($this->redirect('/'));

        return;
    }
}

beforeFilter でリダイレクトする場合、リダイレクト先で改めて beforeFilter が走り、そこで適切に認可チェックされます。したがって、安全に skipAuthorization することができます。

注: skipAuthorization()beforeFilter で呼ぶ必要があります。actionの先頭に置いても、そのaction自体が実行されないので効きません(先の issue #128 での指摘)。


なぜ今まで気づかなかったのか

修正前のコード状況になってしまったのは何故なのか、気になって調べてみました。

CakePHP の変遷をたどると分かったことがありますのでまとめます。

CakePHP redirect() の挙動 beforeFilter で短絡する方法 子クラスからの parent::beforeFilter()
2.x $exit=true実行停止 $this->redirect() を呼べば即停止 影響なし(その場で止まる)
3.x / 4.x Response を返すだけ 実質 return $this->redirect() 子が親クラスの beforeFilter() を呼ぶと、return された値が処理されず、止まらず action が実行されてしまう
5.x 同上 $event->setResult($this->redirect())(5.x で新設) 影響なし(setResult がイベントの結果を書き換えるので止まる)
  • 2.xredirect() がスクリプトを停止していた(2.x API: "Script execution is halted after the redirect.")ので、問題ありませんでした。
  • 3.x→4.xredirect() が停止しなくなり、「return で値を返して止めること」が必要になりましたが、parent::beforeFilter($event); を使うと return された値が処理されず、止まらなくなり、意図せぬ action の実行を招きます。
  • 5.xsetResult で値を渡すようになり、return での値渡しは deprecated になりました。setResult なら、parent::beforeFilter($event); でも問題なく停止します。

つまり今回の 500 は「setResult にしたことで、action の実行がキャンセルされたため、これまで隠れていた問題が顔を出した」ということでした。


関連記事

参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?