Skip to content

Commit 9a75c79

Browse files
committed
Add csrf tokens #70
1 parent a20a2c5 commit 9a75c79

22 files changed

Lines changed: 268 additions & 78 deletions

assets/css/styles.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
1+
/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */
22
@layer properties;
33
@layer theme, base, components, utilities;
44
@layer theme {
@@ -231,6 +231,9 @@
231231
.hidden {
232232
display: none;
233233
}
234+
.inline {
235+
display: inline;
236+
}
234237
.inline-block {
235238
display: inline-block;
236239
}

assets/js/scripts.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ const ajax = (endpoint, callback, data = null, send_json = true) => {
88

99
if (data !== null) {
1010
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
11+
const csrf_meta = document.querySelector('meta[name="csrf-token"]');
12+
const csrf_token = csrf_meta ? encodeURIComponent(csrf_meta.content) : '';
1113

1214
if (send_json) {
13-
data = `${endpoint}=${JSON.stringify(data)}`;
15+
data = `${endpoint}=${JSON.stringify(data)}&csrf_token=${csrf_token}`;
1416
} else {
1517
data = Object.keys(data).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(data[key])).join('&');
18+
data += `&csrf_token=${csrf_token}`;
1619
}
1720
}
1821

@@ -140,7 +143,7 @@ if (delete_all) {
140143

141144
document.getElementById('table-no-keys').classList.remove('hidden');
142145
}
143-
});
146+
}, {});
144147
});
145148
}
146149

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
"watch": "npx @tailwindcss/cli -i ./assets/css/src.css -o ./assets/css/styles.css --watch"
77
},
88
"devDependencies": {
9-
"tailwindcss": "^4.1"
9+
"tailwindcss": "^4.2"
1010
}
1111
}

src/Admin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use RobiNN\Pca\Dashboards\DashboardInterface;
1212

1313
class Admin {
14-
public const VERSION = '2.4.2';
14+
public const VERSION = '2.5.0';
1515

1616
private readonly Template $template;
1717

src/Csrf.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
/**
3+
* This file is part of the phpCacheAdmin.
4+
* Copyright (c) Róbert Kelčák (https://kelcak.com/)
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace RobiNN\Pca;
10+
11+
use Exception;
12+
use RuntimeException;
13+
14+
class Csrf {
15+
public static function generateToken(): string {
16+
$token = Http::session('csrf_token', '');
17+
18+
if ($token === '') {
19+
if (session_status() === PHP_SESSION_NONE) {
20+
session_start();
21+
}
22+
23+
try {
24+
$token = bin2hex(random_bytes(32));
25+
} catch (Exception $e) {
26+
throw new RuntimeException('Could not generate secure random bytes.', 0, $e);
27+
}
28+
29+
$_SESSION['csrf_token'] = $token;
30+
}
31+
32+
return (string) $token;
33+
}
34+
35+
public static function validateToken(?string $token): bool {
36+
$session_token = Http::session('csrf_token', '');
37+
38+
if ($session_token === '' || empty($token)) {
39+
return false;
40+
}
41+
42+
return hash_equals((string) $session_token, $token);
43+
}
44+
}

src/Dashboards/APCu/APCuDashboard.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
namespace RobiNN\Pca\Dashboards\APCu;
1010

11+
use RobiNN\Pca\Csrf;
1112
use RobiNN\Pca\Dashboards\DashboardInterface;
1213
use RobiNN\Pca\Helpers;
14+
use RobiNN\Pca\Http;
1315
use RobiNN\Pca\Template;
1416

1517
class APCuDashboard implements DashboardInterface {
@@ -50,11 +52,21 @@ public function ajax(): string {
5052
return Helpers::getPanelsJson($this->getPanelsData());
5153
}
5254

53-
if (isset($_GET['deleteall']) && apcu_clear_cache()) {
54-
return Helpers::alert($this->template, 'Cache has been cleaned.', 'success');
55+
if (isset($_GET['deleteall'])) {
56+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
57+
return Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
58+
}
59+
60+
if (apcu_clear_cache()) {
61+
return Helpers::alert($this->template, 'Cache has been cleaned.', 'success');
62+
}
5563
}
5664

5765
if (isset($_GET['delete'])) {
66+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
67+
return Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
68+
}
69+
5870
return Helpers::deleteKey($this->template, static fn (string $key): bool => apcu_delete($key), true);
5971
}
6072

src/Dashboards/APCu/APCuTrait.php

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use APCUIterator;
1212
use RobiNN\Pca\Config;
13+
use RobiNN\Pca\Csrf;
1314
use RobiNN\Pca\Format;
1415
use RobiNN\Pca\Helpers;
1516
use RobiNN\Pca\Http;
@@ -112,9 +113,13 @@ private function viewKey(): string {
112113
);
113114
}
114115

115-
if (isset($_GET['delete'])) {
116-
apcu_delete($key);
117-
Http::redirect();
116+
if (isset($_POST['delete'])) {
117+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
118+
Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
119+
} else {
120+
apcu_delete($key);
121+
Http::redirect();
122+
}
118123
}
119124

120125
[$formatted_value, $encode_fn, $is_formatted] = Value::format($value);
@@ -128,7 +133,6 @@ private function viewKey(): string {
128133
'formatted' => $is_formatted,
129134
'edit_url' => Http::queryString(['ttl'], ['form' => 'edit', 'key' => $key]),
130135
'export_url' => Http::queryString(['ttl', 'view', 'p', 'key'], ['export' => 'key']),
131-
'delete_url' => Http::queryString(['view'], ['delete' => 'key', 'key' => $key]),
132136
]);
133137
}
134138

@@ -164,7 +168,11 @@ private function form(): string {
164168
}
165169

166170
if (isset($_POST['submit'])) {
167-
$this->saveKey();
171+
if (Csrf::validateToken(Http::post('csrf_token', ''))) {
172+
$this->saveKey();
173+
} else {
174+
Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
175+
}
168176
}
169177

170178
$value = Value::converter($value, $encoder, 'view');
@@ -297,12 +305,16 @@ public function keysTreeView(array $keys): array {
297305

298306
private function mainDashboard(): string {
299307
if (isset($_POST['submit_import_key'])) {
300-
Helpers::import(
301-
static fn (string $key): bool => apcu_exists($key),
302-
static function (string $key, string $value, int $ttl): bool {
303-
return apcu_store($key, unserialize(base64_decode($value), ['allowed_classes' => false]), $ttl);
304-
}
305-
);
308+
if (Csrf::validateToken(Http::post('csrf_token', ''))) {
309+
Helpers::import(
310+
static fn (string $key): bool => apcu_exists($key),
311+
static function (string $key, string $value, int $ttl): bool {
312+
return apcu_store($key, unserialize(base64_decode($value), ['allowed_classes' => false]), $ttl);
313+
}
314+
);
315+
} else {
316+
echo Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
317+
}
306318
}
307319

308320
$keys = $this->getAllKeys();

src/Dashboards/Memcached/MemcachedDashboard.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace RobiNN\Pca\Dashboards\Memcached;
1010

1111
use RobiNN\Pca\Config;
12+
use RobiNN\Pca\Csrf;
1213
use RobiNN\Pca\Dashboards\DashboardException;
1314
use RobiNN\Pca\Dashboards\DashboardInterface;
1415
use RobiNN\Pca\Helpers;
@@ -98,10 +99,18 @@ public function ajax(): string {
9899
}
99100

100101
if (isset($_GET['deleteall'])) {
102+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
103+
return Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
104+
}
105+
101106
return $this->deleteAllKeys();
102107
}
103108

104109
if (isset($_GET['delete'])) {
110+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
111+
return Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
112+
}
113+
105114
return Helpers::deleteKey($this->template, fn (string $key): bool => $this->memcached->delete($key));
106115
}
107116
} catch (DashboardException|MemcachedException $e) {

src/Dashboards/Memcached/MemcachedTrait.php

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use PDO;
1212
use RobiNN\Pca\Config;
13+
use RobiNN\Pca\Csrf;
1314
use RobiNN\Pca\Format;
1415
use RobiNN\Pca\Helpers;
1516
use RobiNN\Pca\Http;
@@ -148,9 +149,13 @@ private function viewKey(): string {
148149
);
149150
}
150151

151-
if (isset($_GET['delete'])) {
152-
$this->memcached->delete($key);
153-
Http::redirect();
152+
if (isset($_POST['delete'])) {
153+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
154+
Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
155+
} else {
156+
$this->memcached->delete($key);
157+
Http::redirect();
158+
}
154159
}
155160

156161
$value = $this->memcached->get($key);
@@ -166,7 +171,6 @@ private function viewKey(): string {
166171
'formatted' => $is_formatted,
167172
'edit_url' => Http::queryString(['ttl'], ['form' => 'edit', 'key' => $key]),
168173
'export_url' => Http::queryString(['ttl', 'view', 'p', 'key'], ['export' => 'key']),
169-
'delete_url' => Http::queryString(['view'], ['delete' => 'key', 'key' => $key]),
170174
]);
171175
}
172176

@@ -206,7 +210,11 @@ private function form(): string {
206210
}
207211

208212
if (isset($_POST['submit'])) {
209-
$this->saveKey();
213+
if (Csrf::validateToken(Http::post('csrf_token', ''))) {
214+
$this->saveKey();
215+
} else {
216+
Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
217+
}
210218
}
211219

212220
$value = Value::converter($value, $encoder, 'view');
@@ -532,10 +540,14 @@ private function metrics(): string {
532540
*/
533541
private function mainDashboard(): string {
534542
if (isset($_POST['submit_import_key'])) {
535-
Helpers::import(
536-
fn (string $key): bool => $this->memcached->exists($key),
537-
fn (string $key, string $value, int $ttl): bool => $this->memcached->set(urldecode($key), base64_decode($value), $ttl)
538-
);
543+
if (Csrf::validateToken(Http::post('csrf_token', ''))) {
544+
Helpers::import(
545+
fn (string $key): bool => $this->memcached->exists($key),
546+
fn (string $key, string $value, int $ttl): bool => $this->memcached->set(urldecode($key), base64_decode($value), $ttl)
547+
);
548+
} else {
549+
echo Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
550+
}
539551
}
540552

541553
if (Http::get('tab') === 'commands_stats') {

src/Dashboards/OPCache/OPCacheDashboard.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
namespace RobiNN\Pca\Dashboards\OPCache;
1010

11+
use RobiNN\Pca\Csrf;
1112
use RobiNN\Pca\Dashboards\DashboardInterface;
1213
use RobiNN\Pca\Helpers;
14+
use RobiNN\Pca\Http;
1315
use RobiNN\Pca\Template;
1416

1517
class OPCacheDashboard implements DashboardInterface {
@@ -50,11 +52,21 @@ public function ajax(): string {
5052
return Helpers::getPanelsJson($this->getPanelsData());
5153
}
5254

53-
if (isset($_GET['deleteall']) && opcache_reset()) {
54-
return Helpers::alert($this->template, 'Cache has been cleaned.', 'success');
55+
if (isset($_GET['deleteall'])) {
56+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
57+
return Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
58+
}
59+
60+
if (opcache_reset()) {
61+
return Helpers::alert($this->template, 'Cache has been cleaned.', 'success');
62+
}
5563
}
5664

5765
if (isset($_GET['delete'])) {
66+
if (!Csrf::validateToken(Http::post('csrf_token', ''))) {
67+
return Helpers::alert($this->template, 'Invalid CSRF token.', 'error');
68+
}
69+
5870
return Helpers::deleteKey($this->template, static fn (string $key): bool => opcache_invalidate($key, true));
5971
}
6072

0 commit comments

Comments
 (0)