Skip to content

Commit a0eb85d

Browse files
committed
CSRF exclusion for legacy actions
1 parent bf05363 commit a0eb85d

4 files changed

Lines changed: 126 additions & 0 deletions

File tree

yii2-adapter/legacy/web/Controller.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use CraftCms\Cms\Cp\Html\ElementHtml;
1818
use CraftCms\Cms\ProjectConfig\ProjectConfig;
1919
use CraftCms\Cms\User\Elements\User;
20+
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
2021
use Illuminate\Support\Facades\Auth;
2122
use Illuminate\Support\Facades\Gate;
2223
use InvalidArgumentException;
@@ -58,6 +59,17 @@ abstract class Controller extends \yii\web\Controller
5859
public const ALLOW_ANONYMOUS_LIVE = 1;
5960
public const ALLOW_ANONYMOUS_OFFLINE = 2;
6061

62+
public $enableCsrfValidation = true {
63+
get => $this->enableCsrfValidation;
64+
set($value) {
65+
$this->enableCsrfValidation = (bool) $value;
66+
67+
if (!$this->enableCsrfValidation) {
68+
$this->registerCsrfValidationExclusion();
69+
}
70+
}
71+
}
72+
6173
/**
6274
* @var int|bool|int[]|string[] Whether this controller’s actions can be accessed anonymously.
6375
*
@@ -139,6 +151,41 @@ public function init(): void
139151
}
140152

141153
parent::init();
154+
155+
if (!$this->enableCsrfValidation) {
156+
$this->registerCsrfValidationExclusion();
157+
}
158+
}
159+
160+
public function registerCsrfValidationExclusion(): void
161+
{
162+
if (!isset($this->id, $this->module)) {
163+
return;
164+
}
165+
166+
PreventRequestForgery::except($this->csrfValidationExclusionUris());
167+
}
168+
169+
private function csrfValidationExclusionUris(): array
170+
{
171+
$route = trim($this->getUniqueId(), '/');
172+
173+
if ($route === '') {
174+
return [];
175+
}
176+
177+
$actionTrigger = trim(Cms::config()->actionTrigger, '/');
178+
$cpTrigger = trim(Cms::config()->cpTrigger, '/');
179+
180+
return collect([
181+
$route,
182+
implode('/', array_filter([$actionTrigger, $route], fn(string $segment) => $segment !== '')),
183+
implode('/', array_filter([$cpTrigger, $actionTrigger, $route], fn(string $segment) => $segment !== '')),
184+
])
185+
->flatMap(fn(string $route) => [$route, "$route/*"])
186+
->unique()
187+
->values()
188+
->all();
142189
}
143190

144191
/**

yii2-adapter/routes/web.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use CraftCms\Yii2Adapter\Http\ExcludeCsrfValidationForLegacyController;
34
use CraftCms\Yii2Adapter\Http\LegacyMiddleware;
45
use Illuminate\Support\Facades\Route;
56

@@ -11,6 +12,7 @@
1112
})
1213
->middleware([
1314
'craft',
15+
ExcludeCsrfValidationForLegacyController::class,
1416
'craft.web',
1517
LegacyMiddleware::class,
1618
])
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace CraftCms\Yii2Adapter\Http;
4+
5+
use Closure;
6+
use Craft;
7+
use craft\web\Controller;
8+
use Illuminate\Http\Request;
9+
10+
class ExcludeCsrfValidationForLegacyController
11+
{
12+
public function handle(Request $request, Closure $next): mixed
13+
{
14+
if (!$request->isActionRequest()) {
15+
return $next($request);
16+
}
17+
18+
$result = Craft::$app?->createController(implode('/', $request->actionSegments()));
19+
$controller = is_array($result) ? $result[0] : null;
20+
21+
if ($controller instanceof Controller && !$controller->enableCsrfValidation) {
22+
$controller->registerCsrfValidationExclusion();
23+
}
24+
25+
return $next($request);
26+
}
27+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace craft\controllers {
4+
use craft\web\Controller;
5+
use yii\web\Response;
6+
7+
class CsrfCompatibilityController extends Controller
8+
{
9+
public $enableCsrfValidation = false;
10+
11+
protected array|bool|int $allowAnonymous = true;
12+
13+
public function actionPing(): Response
14+
{
15+
return $this->asJson(['ok' => true]);
16+
}
17+
}
18+
}
19+
20+
namespace {
21+
use CraftCms\Cms\Cms;
22+
use CraftCms\Yii2Adapter\Http\ExcludeCsrfValidationForLegacyController;
23+
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
24+
use Illuminate\Http\Request;
25+
26+
it('excludes legacy controller action URLs when CSRF validation is disabled', function() {
27+
$property = new ReflectionProperty(PreventRequestForgery::class, 'neverVerify');
28+
$original = $property->getValue();
29+
30+
try {
31+
$request = Request::create('/actions/csrf-compatibility/ping', 'POST');
32+
33+
app(ExcludeCsrfValidationForLegacyController::class)->handle($request, fn() => response('ok'));
34+
35+
$actionTrigger = trim(Cms::config()->actionTrigger, '/');
36+
$cpTrigger = trim(Cms::config()->cpTrigger, '/');
37+
38+
expect(app(PreventRequestForgery::class)->getExcludedPaths())->toContain(
39+
'csrf-compatibility',
40+
'csrf-compatibility/*',
41+
"$actionTrigger/csrf-compatibility",
42+
"$actionTrigger/csrf-compatibility/*",
43+
"$cpTrigger/$actionTrigger/csrf-compatibility",
44+
"$cpTrigger/$actionTrigger/csrf-compatibility/*",
45+
);
46+
} finally {
47+
$property->setValue(null, $original);
48+
}
49+
});
50+
}

0 commit comments

Comments
 (0)