Bearsampp 2026.7.11
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{
33 private static $fileScanCache = null;
34
39 private static $fileScanCacheDuration = 3600;
40
45 private static $fileScanStats = [
46 'hits' => 0,
47 'misses' => 0,
48 'invalidations' => 0
49 ];
50
56 private static $cacheIntegrityKey = null;
57
66 public static function get($cacheKey)
67 {
68 global $bearsamppRoot;
69
70 // Check if we have in-memory cache first
71 if (self::$fileScanCache !== null && isset(self::$fileScanCache[$cacheKey])) {
72 $cache = self::$fileScanCache[$cacheKey];
73
74 // Check if cache is still valid
75 if (time() - $cache['timestamp'] < self::$fileScanCacheDuration) {
76 return $cache['data'];
77 } else {
78 self::$fileScanStats['invalidations']++;
79 unset(self::$fileScanCache[$cacheKey]);
80 }
81 }
82
83 // Try to load from file cache
84 if (!isset($bearsamppRoot)) {
85 return false;
86 }
87
88 $cacheFile = Path::getTmpPath() . '/filescan_cache_' . $cacheKey . '.dat';
89
90 if (file_exists($cacheFile)) {
91 $fileContents = @file_get_contents($cacheFile);
92
93 if ($fileContents === false) {
94 return false;
95 }
96
97 // Verify HMAC over the raw file bytes BEFORE any parsing.
98 // This prevents PHP object injection via magic methods (__wakeup /
99 // __destruct) that would fire during unserialize() even when the
100 // HMAC subsequently fails.
101 if (!self::verifyCacheIntegrity($fileContents, $cacheKey)) {
102 Log::warning('File scan cache integrity check failed for key: ' . $cacheKey . '. Possible tampering detected.');
103 @unlink($cacheFile);
104 return false;
105 }
106
107 // Integrity confirmed – now it is safe to decode.
108 $cacheData = @json_decode($fileContents, true);
109
110 if (is_array($cacheData)
111 && isset($cacheData['timestamp']) && is_int($cacheData['timestamp'])
112 && isset($cacheData['data']) && is_array($cacheData['data'])
113 && isset($cacheData['hmac']) && is_string($cacheData['hmac'])
114 ) {
115 // Check if file cache is still valid
116 if (time() - $cacheData['timestamp'] < self::$fileScanCacheDuration) {
117 // Store in memory cache for faster subsequent access
118 if (self::$fileScanCache === null) {
119 self::$fileScanCache = [];
120 }
121 self::$fileScanCache[$cacheKey] = $cacheData;
122
123 return $cacheData['data'];
124 } else {
125 // Cache expired, delete file
126 self::$fileScanStats['invalidations']++;
127 @unlink($cacheFile);
128 }
129 } else {
130 // Invalid cache structure, delete file
131 Log::warning('Invalid cache structure detected for key: ' . $cacheKey);
132 @unlink($cacheFile);
133 }
134 }
135
136 return false;
137 }
138
147 public static function set($cacheKey, $data)
148 {
149 global $bearsamppRoot;
150
151 // Generate HMAC for integrity verification
152 $hmac = self::generateCacheHMAC($data, $cacheKey);
153
154 $cacheData = [
155 'timestamp' => time(),
156 'data' => $data,
157 'hmac' => $hmac
158 ];
159
160 // Store in memory cache
161 if (self::$fileScanCache === null) {
162 self::$fileScanCache = [];
163 }
164 self::$fileScanCache[$cacheKey] = $cacheData;
165
166 // Store in file cache
167 if (isset($bearsamppRoot)) {
168 $cacheFile = Path::getTmpPath() . '/filescan_cache_' . $cacheKey . '.dat';
169 // JSON is used instead of serialize() to eliminate the PHP object-injection
170 // attack surface entirely – no magic methods can fire during json_decode().
171 @file_put_contents($cacheFile, json_encode($cacheData), LOCK_EX);
172 Log::debug('File scan results cached to: ' . $cacheFile);
173 }
174 }
175
182 private static function getCacheIntegrityKey()
183 {
184 if (self::$cacheIntegrityKey === null) {
185 global $bearsamppRoot;
186
187 // Try to load existing key from session file
188 if (isset($bearsamppRoot)) {
189 $keyFile = Path::getTmpPath() . '/cache_integrity.key';
190
191 if (file_exists($keyFile)) {
192 $key = @file_get_contents($keyFile);
193 if ($key !== false && strlen($key) === 64) {
194 self::$cacheIntegrityKey = $key;
195 return self::$cacheIntegrityKey;
196 }
197 }
198
199 // Generate new key if none exists or invalid
200 try {
201 self::$cacheIntegrityKey = bin2hex(random_bytes(32));
202 @file_put_contents($keyFile, self::$cacheIntegrityKey, LOCK_EX);
203 } catch (Exception $e) {
204 Log::error('Failed to generate cache integrity key: ' . $e->getMessage());
205 // Fallback to a less secure but functional key
206 self::$cacheIntegrityKey = hash('sha256', uniqid('bearsampp_cache_', true));
207 }
208 } else {
209 // Fallback if bearsamppRoot not available
210 try {
211 self::$cacheIntegrityKey = bin2hex(random_bytes(32));
212 } catch (Exception $e) {
213 self::$cacheIntegrityKey = hash('sha256', uniqid('bearsampp_cache_', true));
214 }
215 }
216 }
217
218 return self::$cacheIntegrityKey;
219 }
220
229 private static function generateCacheHMAC($data, $cacheKey)
230 {
232 // Bind the HMAC to the cache key so a valid payload for key A cannot
233 // be replayed under key B.
234 $message = json_encode($data) . $cacheKey;
235 return hash_hmac('sha256', $message, $key);
236 }
237
246 private static function verifyCacheIntegrity($fileContents, $cacheKey)
247 {
248 // Decode with json_decode() – unlike unserialize(), this cannot
249 // instantiate PHP objects or invoke any magic methods, so it is safe
250 // to call before the HMAC is confirmed.
251 $cacheData = @json_decode($fileContents, true);
252
253 if (!is_array($cacheData)
254 || !isset($cacheData['hmac']) || !is_string($cacheData['hmac'])
255 || !isset($cacheData['data']) || !is_array($cacheData['data'])
256 ) {
257 return false;
258 }
259
260 $expectedHmac = self::generateCacheHMAC($cacheData['data'], $cacheKey);
261
262 // Use hash_equals to prevent timing attacks
263 return hash_equals($expectedHmac, $cacheData['hmac']);
264 }
265
271 public static function clear()
272 {
273 global $bearsamppRoot;
274
275 // Clear memory cache
276 self::$fileScanCache = null;
277
278 // Clear file caches
279 if (isset($bearsamppRoot)) {
280 $tmpPath = Path::getTmpPath();
281 $cacheFiles = glob($tmpPath . '/filescan_cache_*.dat');
282
283 if ($cacheFiles !== false) {
284 foreach ($cacheFiles as $cacheFile) {
285 @unlink($cacheFile);
286 }
287 Log::info('Cleared ' . count($cacheFiles) . ' file scan cache files');
288 }
289 }
290
291 // Reset stats
292 self::$fileScanStats = [
293 'hits' => 0,
294 'misses' => 0,
295 'invalidations' => 0
296 ];
297 }
298
304 public static function getStats()
305 {
306 return self::$fileScanStats;
307 }
308
316 public static function setDuration($seconds)
317 {
318 if ($seconds > 0 && $seconds <= 86400) { // Max 24 hours
319 self::$fileScanCacheDuration = $seconds;
320 Log::debug('File scan cache duration set to ' . $seconds . ' seconds');
321 }
322 }
323
329 public static function getDuration()
330 {
331 return self::$fileScanCacheDuration;
332 }
333
339 public static function recordHit()
340 {
341 self::$fileScanStats['hits']++;
342 }
343
349 public static function recordMiss()
350 {
351 self::$fileScanStats['misses']++;
352 }
353}
354
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)