Bearsampp 2026.3.26
API documentation
Loading...
Searching...
No Matches
class.csrf.php
Go to the documentation of this file.
1<?php
2/*
3 * Copyright (c) 2021-2025 Bearsampp
4 * License: GNU General Public License version 3 or later; see LICENSE.txt
5 * Author: Bear
6 * Website: https://bearsampp.com
7 * Github: https://github.com/Bearsampp
8 */
9
34class Csrf
35{
39 const SESSION_KEY = 'bearsampp_csrf_tokens';
40
44 const TOKEN_EXPIRATION = 7200;
45
50 const MAX_TOKENS = 10;
51
58 public static function init()
59 {
60 // Start session if not already started
61 if (session_status() === PHP_SESSION_NONE) {
62 session_start();
63 }
64
65 // Initialize token storage if not exists
66 if (!isset($_SESSION[self::SESSION_KEY])) {
67 $_SESSION[self::SESSION_KEY] = [];
68 }
69
70 // Clean up expired tokens
72 }
73
80 public static function generateToken()
81 {
82 self::init();
83
84 try {
85 // Generate cryptographically secure random token
86 $token = bin2hex(random_bytes(32));
87 } catch (Exception $e) {
88 Util::logError('Failed to generate CSRF token: ' . $e->getMessage());
89 // Fallback to less secure but functional method
90 $token = hash('sha256', uniqid('bearsampp_csrf_', true) . microtime(true));
91 }
92
93 // Store token with timestamp
94 $_SESSION[self::SESSION_KEY][$token] = time();
95
96 // Limit number of stored tokens
97 if (count($_SESSION[self::SESSION_KEY]) > self::MAX_TOKENS) {
98 // Remove oldest token
99 $oldestToken = array_key_first($_SESSION[self::SESSION_KEY]);
100 unset($_SESSION[self::SESSION_KEY][$oldestToken]);
101 }
102
103 // Log token generation without exposing token material
104 Util::logDebug('CSRF token generated successfully');
105
106 return $token;
107 }
108
114 public static function getToken()
115 {
116 self::init();
117
118 // If no tokens exist, generate one
119 if (empty($_SESSION[self::SESSION_KEY])) {
120 return self::generateToken();
121 }
122
123 // Return the most recent token
124 $tokens = $_SESSION[self::SESSION_KEY];
125 end($tokens);
126 $latestToken = key($tokens);
127
128 // Check if latest token is expired
129 if (time() - $tokens[$latestToken] > self::TOKEN_EXPIRATION) {
130 // Generate new token if expired
131 return self::generateToken();
132 }
133
134 return $latestToken;
135 }
136
144 public static function validateToken($token, $removeAfterValidation = false)
145 {
146 self::init();
147
148 // Check if token is provided
149 if (empty($token) || !is_string($token)) {
150 Util::logWarning('CSRF validation failed: No token provided');
151 return false;
152 }
153
154 // Check if token exists in session
155 if (!isset($_SESSION[self::SESSION_KEY][$token])) {
156 Util::logWarning('CSRF validation failed: Token not found in session');
157 return false;
158 }
159
160 // Check if token is expired
161 $tokenTimestamp = $_SESSION[self::SESSION_KEY][$token];
162 if (time() - $tokenTimestamp > self::TOKEN_EXPIRATION) {
163 Util::logWarning('CSRF validation failed: Token expired');
164 unset($_SESSION[self::SESSION_KEY][$token]);
165 return false;
166 }
167
168 // Token is valid
169 Util::logDebug('CSRF token validated successfully');
170
171 // Remove token if one-time use is requested
172 if ($removeAfterValidation) {
173 unset($_SESSION[self::SESSION_KEY][$token]);
174 }
175
176 return true;
177 }
178
186 public static function validateRequest($removeAfterValidation = false)
187 {
188 // Check POST first (most common for AJAX)
189 if (isset($_POST['csrf_token'])) {
190 return self::validateToken($_POST['csrf_token'], $removeAfterValidation);
191 }
192
193 // Check GET as fallback
194 if (isset($_GET['csrf_token'])) {
195 return self::validateToken($_GET['csrf_token'], $removeAfterValidation);
196 }
197
198 // Check custom header (for AJAX requests)
199 $headers = self::getAllHeaders();
200
201 // Check for X-CSRF-Token header (case-insensitive)
202 foreach ($headers as $key => $value) {
203 if (strtolower($key) === 'x-csrf-token') {
204 return self::validateToken($value, $removeAfterValidation);
205 }
206 }
207
208 Util::logWarning('CSRF validation failed: No token in request');
209 return false;
210 }
211
218 private static function getAllHeaders()
219 {
220 // Use getallheaders() if available (Apache)
221 if (function_exists('getallheaders')) {
222 $headers = getallheaders();
223 if ($headers !== false) {
224 return $headers;
225 }
226 }
227
228 // Fallback for FastCGI/CGI environments
229 $headers = [];
230 foreach ($_SERVER as $key => $value) {
231 // Extract HTTP headers from $_SERVER
232 if (substr($key, 0, 5) === 'HTTP_') {
233 // Convert HTTP_X_CSRF_TOKEN to X-Csrf-Token
234 $headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
235 $headers[$headerName] = $value;
236 }
237 // Handle CONTENT_TYPE and CONTENT_LENGTH specially
238 elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
239 $headerName = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));
240 $headers[$headerName] = $value;
241 }
242 }
243
244 return $headers;
245 }
246
252 private static function cleanupExpiredTokens()
253 {
254 if (!isset($_SESSION[self::SESSION_KEY])) {
255 return 0;
256 }
257
258 $removed = 0;
259 $currentTime = time();
260
261 foreach ($_SESSION[self::SESSION_KEY] as $token => $timestamp) {
262 if ($currentTime - $timestamp > self::TOKEN_EXPIRATION) {
263 unset($_SESSION[self::SESSION_KEY][$token]);
264 $removed++;
265 }
266 }
267
268 if ($removed > 0) {
269 Util::logDebug("Cleaned up $removed expired CSRF tokens");
270 }
271
272 return $removed;
273 }
274
281 public static function regenerateToken()
282 {
283 self::init();
284
285 // Clear all existing tokens
286 $_SESSION[self::SESSION_KEY] = [];
287
288 // Generate new token
289 return self::generateToken();
290 }
291
297 public static function getTokenField()
298 {
299 $token = self::getToken();
300 return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token, ENT_QUOTES, 'UTF-8') . '">';
301 }
302
309 public static function getTokenMeta()
310 {
311 $token = self::getToken();
312 return '<meta name="csrf-token" content="' . htmlspecialchars($token, ENT_QUOTES, 'UTF-8') . '">';
313 }
314
322 public static function validateOrDie($removeAfterValidation = false)
323 {
324 if (!self::validateRequest($removeAfterValidation)) {
325 http_response_code(403);
326 header('Content-Type: application/json');
327 echo json_encode([
328 'error' => 'CSRF validation failed',
329 'message' => 'Invalid or expired security token. Please refresh the page and try again.'
330 ]);
331 exit;
332 }
333 }
334
341 public static function getStats()
342 {
343 self::init();
344
345 $tokens = $_SESSION[self::SESSION_KEY] ?? [];
346 $currentTime = time();
347 $expired = 0;
348 $valid = 0;
349
350 foreach ($tokens as $timestamp) {
351 if ($currentTime - $timestamp > self::TOKEN_EXPIRATION) {
352 $expired++;
353 } else {
354 $valid++;
355 }
356 }
357
358 return [
359 'total' => count($tokens),
360 'valid' => $valid,
361 'expired' => $expired,
362 'max_tokens' => self::MAX_TOKENS,
363 'expiration_seconds' => self::TOKEN_EXPIRATION
364 ];
365 }
366}
static generateToken()
static cleanupExpiredTokens()
const MAX_TOKENS
const TOKEN_EXPIRATION
static regenerateToken()
static getTokenField()
static getStats()
static getToken()
static validateOrDie($removeAfterValidation=false)
static validateRequest($removeAfterValidation=false)
static getAllHeaders()
static getTokenMeta()
static validateToken($token, $removeAfterValidation=false)
static init()
const SESSION_KEY
static logError($data, $file=null)
static logDebug($data, $file=null)
static logWarning($data, $file=null)