Skip to content

Commit f06ff3a

Browse files
committed
Add CSRF token security to the realtime compiler dashboard
1 parent 48f1f58 commit f06ff3a

File tree

3 files changed

+50
-1
lines changed

3 files changed

+50
-1
lines changed

packages/realtime-compiler/resources/dashboard.blade.php

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<head>
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<meta name="csrf-token" content="{{ $csrfToken }}">
78
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
89
<title>{{ $title }}</title>
910
<base target="_parent">
@@ -64,6 +65,7 @@
6465
<h2 class="h5 mb-0">Site Pages & Routes</h2>
6566
@if($dashboard->isInteractive())
6667
<form class="buttonActionForm" action="" method="POST">
68+
<input type="hidden" name="_token" value="{{ $csrfToken }}">
6769
<input type="hidden" name="action" value="openInExplorer">
6870
<button type="submit" class="btn btn-outline-primary btn-sm" title="Open project in system file explorer">Open folder</button>
6971
</form>
@@ -136,6 +138,7 @@
136138
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
137139
</div>
138140
<form id="createPageForm" action="" method="POST">
141+
<input type="hidden" name="_token" value="{{ $csrfToken }}">
139142
<input type="hidden" name="action" value="createPage">
140143

141144
<div class="modal-body">
@@ -242,6 +245,7 @@
242245
<div class="d-flex justify-content-end">
243246
@if($dashboard->isInteractive())
244247
<form class="buttonActionForm" action="" method="POST">
248+
<input type="hidden" name="_token" value="{{ $csrfToken }}">
245249
<input type="hidden" name="action" value="openPageInEditor">
246250
<input type="hidden" name="routeKey" value="{{ $route->getRouteKey() }}">
247251
<button type="submit" class="btn btn-outline-primary btn-sm me-2" title="Open in system default application">Edit</button>
@@ -302,6 +306,7 @@
302306
@if($dashboard->isInteractive())
303307
<div class="w-auto ps-0">
304308
<form class="buttonActionForm" action="" method="POST">
309+
<input type="hidden" name="_token" value="{{ $csrfToken }}">
305310
<input type="hidden" name="action" value="openMediaFileInEditor">
306311
<input type="hidden" name="identifier" value="{{ $mediaFile->getIdentifier() }}">
307312
<button type="submit" class="btn btn-link btn-sm py-0" title="Open this image in the system editor">Edit</button>

packages/realtime-compiler/src/Http/BaseController.php

+44
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Hyde\RealtimeCompiler\Http;
66

7+
use BadMethodCallException;
78
use Desilva\Microserve\Request;
89
use Desilva\Microserve\Response;
910
use Desilva\Microserve\JsonResponse;
@@ -76,6 +77,14 @@ protected function authorizePostRequest(): void
7677
if (! $this->isRequestMadeFromLocalhost()) {
7778
throw new HttpException(403, "Refusing to serve request from address {$_SERVER['REMOTE_ADDR']} (must be on localhost)");
7879
}
80+
81+
if ($this->withSession) {
82+
if (! $this->validateCSRFToken($this->request->get('_token'))) {
83+
throw new HttpException(403, 'Invalid CSRF token');
84+
} else {
85+
$this->expireCSRFToken();
86+
}
87+
}
7988
}
8089

8190
protected function isRequestMadeFromLocalhost(): bool
@@ -91,6 +100,41 @@ protected function isRequestMadeFromLocalhost(): bool
91100
return in_array($requestIp, $allowedIps, true);
92101
}
93102

103+
protected function generateCSRFToken(): string
104+
{
105+
if (session_status() !== PHP_SESSION_ACTIVE) {
106+
throw new BadMethodCallException('Session not started');
107+
}
108+
109+
if (empty($_SESSION['csrf_token'])) {
110+
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
111+
}
112+
113+
return $_SESSION['csrf_token'];
114+
}
115+
116+
protected function validateCSRFToken(?string $suppliedToken): bool
117+
{
118+
if (session_status() !== PHP_SESSION_ACTIVE) {
119+
throw new BadMethodCallException('Session not started');
120+
}
121+
122+
if ($suppliedToken === null) {
123+
return false;
124+
}
125+
126+
return ! empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $suppliedToken);
127+
}
128+
129+
protected function expireCSRFToken(): void
130+
{
131+
if (session_status() !== PHP_SESSION_ACTIVE) {
132+
throw new BadMethodCallException('Session not started');
133+
}
134+
135+
unset($_SESSION['csrf_token']);
136+
}
137+
94138
protected function writeToConsole(string $message, string $context = 'dashboard'): void
95139
{
96140
if (isset($this->console)) {

packages/realtime-compiler/src/Http/DashboardController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public function handle(): Response
9393
protected function show(): string
9494
{
9595
return AnonymousViewCompiler::handle(__DIR__.'/../../resources/dashboard.blade.php', array_merge(
96-
(array) $this, ['dashboard' => $this, 'request' => $this->request],
96+
(array) $this, ['dashboard' => $this, 'request' => $this->request, 'csrfToken' => $this->generateCSRFToken()],
9797
));
9898
}
9999

0 commit comments

Comments
 (0)