Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.cache.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2022-2025 Bearsampp
5 * * License: GNU General Public License version 3 or later; see LICENSE.txt
6 * * Website: https://bearsampp.com
7 * * Github: https://github.com/Bearsampp
8 *
9 */
10
27class Cache
28{
34 private static $fileScanCache = null;
35
41 private static $fileScanCacheDuration = 3600;
42
48 private static $fileScanStats = [
49 'hits' => 0,
50 'misses' => 0,
51 'invalidations' => 0
52 ];
53
60 private static $cacheIntegrityKey = null;
61
70 public static function get($cacheKey)
71 {
72 global $bearsamppRoot;
73
74 // Check if we have in-memory cache first
75 if (self::$fileScanCache !== null && isset(self::$fileScanCache[$cacheKey])) {
76 $cache = self::$fileScanCache[$cacheKey];
77
78 // Check if cache is still valid
79 if (time() - $cache['timestamp'] < self::$fileScanCacheDuration) {
80 return $cache['data'];
81 } else {
82 self::$fileScanStats['invalidations']++;
83 unset(self::$fileScanCache[$cacheKey]);
84 }
85 }
86
87 // Try to load from file cache
88 if (!isset($bearsamppRoot)) {
89 return false;
90 }
91
92 $cacheFile = Path::getTmpPath() . '/filescan_cache_' . $cacheKey . '.dat';
93
94 if (file_exists($cacheFile)) {
95 $fileContents = @file_get_contents($cacheFile);
96
97 if ($fileContents === false) {
98 return false;
99 }
100
101 // Verify HMAC over the raw file bytes BEFORE any parsing.
102 // This prevents PHP object injection via magic methods (__wakeup /
103 // __destruct) that would fire during unserialize() even when the
104 // HMAC subsequently fails.
105 if (!self::verifyCacheIntegrity($fileContents, $cacheKey)) {
106 Log::warning('File scan cache integrity check failed for key: ' . $cacheKey . '. Possible tampering detected.');
107 @unlink($cacheFile);
108
109 return false;
110 }
111
112 // Integrity confirmed – now it is safe to decode.
113 $cacheData = @json_decode($fileContents, true);
114
115 if (is_array($cacheData)
116 && isset($cacheData['timestamp']) && is_int($cacheData['timestamp'])
117 && isset($cacheData['data']) && is_array($cacheData['data'])
118 && isset($cacheData['hmac']) && is_string($cacheData['hmac'])
119 ) {
120 // Check if file cache is still valid
121 if (time() - $cacheData['timestamp'] < self::$fileScanCacheDuration) {
122 // Store in memory cache for faster subsequent access
123 if (self::$fileScanCache === null) {
124 self::$fileScanCache = [];
125 }
126 self::$fileScanCache[$cacheKey] = $cacheData;
127
128 return $cacheData['data'];
129 } else {
130 // Cache expired, delete file
131 self::$fileScanStats['invalidations']++;
132 @unlink($cacheFile);
133 }
134 } else {
135 // Invalid cache structure, delete file
136 Log::warning('Invalid cache structure detected for key: ' . $cacheKey);
137 @unlink($cacheFile);
138 }
139 }
140
141 return false;
142 }
143
152 public static function set($cacheKey, $data)
153 {
154 global $bearsamppRoot;
155
156 // Generate HMAC for integrity verification
157 $hmac = self::generateCacheHMAC($data, $cacheKey);
158
159 $cacheData = [
160 'timestamp' => time(),
161 'data' => $data,
162 'hmac' => $hmac
163 ];
164
165 // Store in memory cache
166 if (self::$fileScanCache === null) {
167 self::$fileScanCache = [];
168 }
169 self::$fileScanCache[$cacheKey] = $cacheData;
170
171 // Store in file cache
172 if (isset($bearsamppRoot)) {
173 $cacheFile = Path::getTmpPath() . '/filescan_cache_' . $cacheKey . '.dat';
174 // JSON is used instead of serialize() to eliminate the PHP object-injection
175 // attack surface entirely – no magic methods can fire during json_decode().
176 @file_put_contents($cacheFile, json_encode($cacheData), LOCK_EX);
177 Log::debug('File scan results cached to: ' . $cacheFile);
178 }
179 }
180
187 private static function getCacheIntegrityKey()
188 {
189 if (self::$cacheIntegrityKey === null) {
190 global $bearsamppRoot;
191
192 // Try to load existing key from session file
193 if (isset($bearsamppRoot)) {
194 $keyFile = Path::getTmpPath() . '/cache_integrity.key';
195
196 if (file_exists($keyFile)) {
197 $key = @file_get_contents($keyFile);
198 if ($key !== false && strlen($key) === 64) {
199 self::$cacheIntegrityKey = $key;
200
201 return self::$cacheIntegrityKey;
202 }
203 }
204
205 // Generate new key if none exists or invalid
206 try {
207 self::$cacheIntegrityKey = bin2hex(random_bytes(32));
208 @file_put_contents($keyFile, self::$cacheIntegrityKey, LOCK_EX);
209 } catch (Exception $e) {
210 Log::error('Failed to generate cache integrity key: ' . $e->getMessage());
211 // Fallback to a less secure but functional key
212 self::$cacheIntegrityKey = hash('sha256', uniqid('bearsampp_cache_', true));
213 }
214 } else {
215 // Fallback if bearsamppRoot not available
216 try {
217 self::$cacheIntegrityKey = bin2hex(random_bytes(32));
218 } catch (Exception $e) {
219 self::$cacheIntegrityKey = hash('sha256', uniqid('bearsampp_cache_', true));
220 }
221 }
222 }
223
224 return self::$cacheIntegrityKey;
225 }
226
235 private static function generateCacheHMAC($data, $cacheKey)
236 {
238 // Bind the HMAC to the cache key so a valid payload for key A cannot
239 // be replayed under key B.
240 $message = json_encode($data) . $cacheKey;
241
242 return hash_hmac('sha256', $message, $key);
243 }
244
253 private static function verifyCacheIntegrity($fileContents, $cacheKey)
254 {
255 // Decode with json_decode() – unlike unserialize(), this cannot
256 // instantiate PHP objects or invoke any magic methods, so it is safe
257 // to call before the HMAC is confirmed.
258 $cacheData = @json_decode($fileContents, true);
259
260 if (!is_array($cacheData)
261 || !isset($cacheData['hmac']) || !is_string($cacheData['hmac'])
262 || !isset($cacheData['data']) || !is_array($cacheData['data'])
263 ) {
264 return false;
265 }
266
267 $expectedHmac = self::generateCacheHMAC($cacheData['data'], $cacheKey);
268
269 // Use hash_equals to prevent timing attacks
270 return hash_equals($expectedHmac, $cacheData['hmac']);
271 }
272
278 public static function clear()
279 {
280 global $bearsamppRoot;
281
282 // Clear memory cache
283 self::$fileScanCache = null;
284
285 // Clear file caches
286 if (isset($bearsamppRoot)) {
287 $tmpPath = Path::getTmpPath();
288 $cacheFiles = glob($tmpPath . '/filescan_cache_*.dat');
289
290 if ($cacheFiles !== false) {
291 foreach ($cacheFiles as $cacheFile) {
292 @unlink($cacheFile);
293 }
294 Log::info('Cleared ' . count($cacheFiles) . ' file scan cache files');
295 }
296 }
297
298 // Reset stats
299 self::$fileScanStats = [
300 'hits' => 0,
301 'misses' => 0,
302 'invalidations' => 0
303 ];
304 }
305
311 public static function getStats()
312 {
313 return self::$fileScanStats;
314 }
315
323 public static function setDuration($seconds)
324 {
325 if ($seconds > 0 && $seconds <= 86400) { // Max 24 hours
326 self::$fileScanCacheDuration = $seconds;
327 Log::debug('File scan cache duration set to ' . $seconds . ' seconds');
328 }
329 }
330
336 public static function getDuration()
337 {
338 return self::$fileScanCacheDuration;
339 }
340
346 public static function recordHit()
347 {
348 self::$fileScanStats['hits']++;
349 }
350
356 public static function recordMiss()
357 {
358 self::$fileScanStats['misses']++;
359 }
360}
361
global $bearsamppRoot
static recordHit()
static $fileScanStats
static clear()
static getStats()
static $cacheIntegrityKey
static getCacheIntegrityKey()
static $fileScanCache
static $fileScanCacheDuration
static generateCacheHMAC($data, $cacheKey)
static getDuration()
static recordMiss()
static setDuration($seconds)
static verifyCacheIntegrity($fileContents, $cacheKey)
static info($data, $file=null)
static debug($data, $file=null)
static warning($data, $file=null)
static error($data, $file=null)
static getTmpPath($aetrayPath=false)