FRED™  3.0
FRED™: Framework for Rapid and Easy Development
Security.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Rsi\Fred;
4 
5 class Security extends Component{
6 
7  public $proxies = []; //!< Proxy white-list (key = proxy IP-address (optionally in CIDR notation), value = header to use
8  // (string) or headers (array)).
9  public $allowlist = []; //!< IP-addresses (key; CIDR notation; seperate with semi-colon) to exclude from certain checks
10  // (value = array of check names).
11  public $blocklist = []; //!< IP-addresses to ban anyhow (array of IP-address, optionally in CIDR notation).
12  public $ext = '.ban'; //!< Extension for ban registration file.
13  public $purgeInterval = 600; //!< Minimum interval between purges.
14  public $defaultPurgeDays = 14; //!< Default number of days after which a ban file will be purged.
15  public $defaultDelay = 3600; //!< Default time (seconds) a ban reason stays in the registry.
16  public $bruteForceDelay = 10; //!< Default time (seconds) a brute force check stays in the registry.
17  public $banCount = 5; //!< Number of registrations that will get you banned.
18  public $server = []; //!< Server checks configuration (key = name, value = config as assoc.array).
19 
20  protected $_path = null; //!< Path to store the ban registration files (temp path if empty).
21  protected $_checks = null; //!< Available checks (key = name, value = \\Rsi\\Fred\\Security\\Check).
22 
23  protected $_results = []; //!< Results of checks (only check once).
24  protected $_remoteAddr = null; //!< True IP-address of the client client (optionally behind a white-listed proxy).
25  protected $_filename = null;
26  protected $_banned = null;
27 
28  public function clientConfig(){
29  $config = [];
30  foreach($this->checks as $name => $check) $config[$name] = $check->clientConfig();
31  return array_filter($config);
32  }
33 
34  protected function filename($addr){
35  return $this->path . str_replace(['.',':'],'-',$addr) . $this->ext;
36  }
37 
38  protected function hasBan(){
39  return is_file($this->filename);
40  }
41 
42  protected function countBan(){
43  return $this->hasBan() ? count(file($this->filename,FILE_SKIP_EMPTY_LINES)) : 0;
44  }
45 
46  protected function writeBan($reason,$time){
47  return \Rsi\File::write($this->filename,"$time:$reason\n",0666,true);
48  }
49  /**
50  * Add a ban reason to the registry.
51  * @param string $reason Name of reason.
52  * @param int $delay Time (seconds) the reason should stay in the registry (empty = use default).
53  * @return bool True when successful (null if the reason was on the allowlist).
54  */
55  public function addBan($reason,$delay = null){
56  if($this->remoteAddr) try{
57  foreach($this->allowlist as $subnets => $reasons)
58  if(in_array($reason,$reasons) && \Rsi\Http::inSubnet(explode(';',$subnets),$this->remoteAddr)) return null;
59  if(($this->countBan() < 10 * $this->banCount) && $this->writeBan($reason,time() + ($delay ?: $this->defaultDelay)))
60  $this->session->banned = $this->_banned = null; //determine again on next call
61  }
62  catch(Exception $e){
63  if($this->_fred->debug) throw $e;
64  return false;
65  }
66  return true;
67  }
68  /**
69  * Add a brute force reason to the registry.
70  * @param bool $result Whether the request was OK or not (only negative results get registered; however, positive results
71  * with a non-empty registry will be delayed too - this prevents attackers from interrupting a request after a small amount
72  * of time).
73  * @param string $reason Name of reason.
74  * @param int $delay Time (seconds) the reason should stay in the registry (empty = default).
75  */
76  public function bruteForceDelay($result,$reason = null,$delay = null){
77  if(!$result) $this->addBan($reason ?: 'brute',$delay ?: $this->bruteForceDelay);
78  if($this->hasBan()) sleep(ceil($this->bruteForceDelay * ($result ? pow(rand(0,100),3) / 1000000 : 1)));
79  }
80  /**
81  * Unban a client's IP address.
82  * @param string $addr
83  * @return bool True if the ban was successful deleted.
84  */
85  public function unBan($addr){
86  $result = true;
87  foreach($this->checks as $name=> $check) if(!$check->unBan($addr)) $result = false;
88  return \Rsi\File::unlink($this->filename($addr)) && $result;
89  }
90  /**
91  * Perform a server check.
92  * @param string $name Name of the check.
93  * @return mixed Whatever the check returned
94  */
95  public function server($name){
96  $config = $this->server[$name] ?? [];
97  $class_name = \Rsi\Record::get($config,'className',__CLASS__ . '\\Server\\' . ucfirst($name));
98  $check = new $class_name($this->_fred,$config);
99  return $check->check();
100  }
101  /**
102  * Purge the ban registration files.
103  * @param int $days Number of days after which a file should be purged (defaultPurgeDays when null)
104  * @return int Number of files purged. Null when process locked.
105  */
106  public function purge($days = null){
107  if(is_file($filename = $this->_path . 'purge.lock') && (filemtime($filename) > time() - $this->purgeInterval)) return null;
108  file_put_contents($filename,date('c'));
109  $time = time() - 86400 * ($days === null ? $this->defaultPurgeDays : $days);
110  $count = 0;
111  foreach((new \GlobIterator($this->path . '*' . $this->ext)) as $filename => $file)
112  if($file->getMTime() < $time) $count += \Rsi\File::unlink($filename);
113  return $count;
114  }
115  /**
116  * Perform all security checks.
117  * @param array|bool $ignore Checks to ignore (true = all).
118  * @param bool $expected True if this is an expected call.
119  * @return bool True if all checks are fine.
120  */
121  public function check($ignore = null,$expected = false){
122  $result = true;
123  if(!$this->banned){
124  if($blocked = $this->session->blocked) $this->_banned = true;
125  elseif($blocked === null) $this->session->blocked = $this->_banned = \Rsi\Http::inSubnet($this->blocklist,$this->remoteAddr);
126  if(!$this->_banned && ($ignore !== true)){
127  if(($allowed = $this->session->allowed) === null){
128  $allowed = [];
129  foreach($this->allowlist as $subnets => $checks) if(\Rsi\Http::inSubnet(explode(';',$subnets),$this->remoteAddr))
130  $allowed = array_merge($allowed,$checks);
131  $this->session->allowed = $allowed;
132  }
133  $ignore = array_merge($ignore ?: [],$allowed);
134  foreach($this->checks as $name => $check) if(!is_array($ignore) || !in_array($name,$ignore)){
135  if(!array_key_exists($name,$this->_results)) $this->_results[$name] = $check->check($expected);
136  if(!$this->_results[$name]){
137  $this->component('log')->notice("Suspicious $name request");
138  if($this->_results[$name] !== null) $this->addBan($name,$check->delay);
139  break;
140  }
141  }
142  }
143  if($this->banned){
144  $this->server('config');
145  $this->purge();
146  }
147  }
148  if($this->banned){
149  http_response_code(429); //Too Many Requests
150  try{
151  $_SERVER['REMOTE_ADDR'] = $this->remoteAddr;
152  require($this->_fred->templatePath . 'banned.php');
153  }
154  catch(Exception $e){
155  print('Banned');
156  }
157  $this->_fred->halt();
158  }
159  return $result;
160  }
161 
162  protected function getBanned(){
163  if(($this->_banned === null) && !($this->_banned = $this->session->banned)){
164  if($this->_banned = is_file($this->filename)) try{ //in case of error: banned (probably file access problems due to brute forcing)
165  $reasons = [];
166  $purged = false;
167  foreach(file($this->filename) as $reason)
168  if(substr($reason,0,strpos($reason,':')) >= time()) $reasons[] = $reason;
169  else $purged = true;
170  if($purged){
171  if($reasons) \Rsi\File::write($this->filename,implode($reasons),0666);
172  else \Rsi\File::unlink($this->filename);
173  }
174  $this->_banned = count($reasons) >= $this->banCount;
175  }
176  catch(Exception $e){
177  if($this->_fred->debug) throw $e;
178  }
179  $this->session->banned = $this->_banned;
180  }
181  return $this->_banned;
182  }
183 
184  protected function getChecks(){
185  if($this->_checks === null){
186  $this->_checks = [];
187  foreach($this->config('checks',[]) as $name => $config){
188  $class_name = \Rsi\Record::get($config,'className',__CLASS__ . '\\Check\\' . ucfirst($name));
189  $this->_checks[$name] = new $class_name($this->_fred,$config);
190  }
191  }
192  return $this->_checks;
193  }
194 
195  protected function getFilename(){
196  if(!$this->_filename) $this->_filename = $this->filename($this->remoteAddr);
197  return $this->_filename;
198  }
199 
200  protected function getPath(){
201  if(!$this->_path) $this->_path = $this->config('path') ?: \Rsi\File::tempDir();
202  return $this->_path;
203  }
204 
205  protected function getRemoteAddr(){
206  if(($this->_remoteAddr === null) && !\Rsi::commandLine()){
207  $this->_remoteAddr = $remote_addr = \Rsi\Http::remoteAddr();
208  foreach($this->proxies as $subnets => $headers) if(\Rsi\Http::inSubnet(explode(';',$subnets),$remote_addr)){ //applicable proxy
209  $reason = 'missing';
210  foreach((array)$headers as $header) if(array_key_exists($header,$_SERVER)) foreach(explode(',',$_SERVER[$header]) as $addr){
211  if(filter_var($addr = trim($addr),FILTER_VALIDATE_IP,FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false)
212  return $this->_remoteAddr = \Rsi\Http::expandAddr($addr);
213  $reason = 'invalid';
214  }
215  $this->component('log')->warning(
216  "Proxy request from $remote_addr has $reason IP forward header",
217  __FILE__,
218  __LINE__,
219  compact('subnets','headers') + ['remoteAddr' => $remote_addr,'http' => $this->component('request')->server->http->data,'request' => $_REQUEST]
220  );
221  $this->_remoteAddr = false;
222  }
223  }
224  return $this->_remoteAddr;
225  }
226 
227 }
Rsi\Fred\Security\$_results
$_results
Results of checks (only check once).
Definition: Security.php:23
Rsi\Fred\Security\getChecks
getChecks()
Definition: Security.php:184
Rsi\Fred\Security\$allowlist
$allowlist
IP-addresses (key; CIDR notation; seperate with semi-colon) to exclude from certain checks.
Definition: Security.php:9
Rsi\Fred\Security\$_path
$_path
Path to store the ban registration files (temp path if empty).
Definition: Security.php:20
Rsi
Rsi\Fred\Security\filename
filename($addr)
Definition: Security.php:34
Rsi\Fred\Security\getFilename
getFilename()
Definition: Security.php:195
Rsi\Fred\Security\$bruteForceDelay
$bruteForceDelay
Default time (seconds) a brute force check stays in the registry.
Definition: Security.php:16
Rsi\Fred\Security\getBanned
getBanned()
Definition: Security.php:162
Rsi\Fred\Security
Definition: Security.php:5
Rsi\Fred\Component\filemtime
filemtime($filename)
Filemtime with session cache.
Definition: Component.php:66
Rsi\Fred\Security\$_filename
$_filename
Definition: Security.php:25
Rsi\Fred\Security\purge
purge($days=null)
Purge the ban registration files.
Definition: Security.php:106
Rsi\Fred\Security\server
server($name)
Perform a server check.
Definition: Security.php:95
Rsi\Fred\Security\writeBan
writeBan($reason, $time)
Definition: Security.php:46
Rsi\Fred\Security\hasBan
hasBan()
Definition: Security.php:38
Rsi\Fred\Component
Basic component class.
Definition: Component.php:8
Rsi\Fred\Security\$_banned
$_banned
Definition: Security.php:26
Rsi\Fred\Security\$ext
$ext
Extension for ban registration file.
Definition: Security.php:12
Rsi\Fred\Security\bruteForceDelay
bruteForceDelay($result, $reason=null, $delay=null)
Add a brute force reason to the registry.
Definition: Security.php:76
Rsi\Fred\Component\component
component($name)
Get a component (local or default).
Definition: Component.php:81
Rsi\Fred\Security\$defaultPurgeDays
$defaultPurgeDays
Default number of days after which a ban file will be purged.
Definition: Security.php:14
Rsi\Fred\Security\$_checks
$_checks
Available checks (key = name, value = \Rsi\Fred\Security\Check).
Definition: Security.php:21
Rsi\Fred\Component\config
config($key, $default=null)
Retrieve a config value.
Definition: Component.php:53
Rsi\Fred\Security\clientConfig
clientConfig()
Public configuration.
Definition: Security.php:28
Rsi\Fred\Security\unBan
unBan($addr)
Unban a client's IP address.
Definition: Security.php:85
Rsi\Fred\Security\$_remoteAddr
$_remoteAddr
True IP-address of the client client (optionally behind a white-listed proxy).
Definition: Security.php:24
Rsi\Fred\Security\$server
$server
Server checks configuration (key = name, value = config as assoc.array).
Definition: Security.php:18
Rsi\Fred\Security\getRemoteAddr
getRemoteAddr()
Definition: Security.php:205
Rsi\Fred\Security\$defaultDelay
$defaultDelay
Default time (seconds) a ban reason stays in the registry.
Definition: Security.php:15
Rsi\Fred\Security\$banCount
$banCount
Number of registrations that will get you banned.
Definition: Security.php:17
Rsi\Fred\Security\$purgeInterval
$purgeInterval
Minimum interval between purges.
Definition: Security.php:13
Rsi\Fred\Security\countBan
countBan()
Definition: Security.php:42
Rsi\Fred\Security\getPath
getPath()
Definition: Security.php:200
Rsi\Fred\Security\$proxies
$proxies
Proxy white-list (key = proxy IP-address (optionally in CIDR notation), value = header to use.
Definition: Security.php:7
Rsi\Fred\Security\addBan
addBan($reason, $delay=null)
Add a ban reason to the registry.
Definition: Security.php:55
Rsi\Fred\Security\check
check($ignore=null, $expected=false)
Perform all security checks.
Definition: Security.php:121
Rsi\Fred\Exception
Definition: Exception.php:5
Rsi\Fred\Security\$blocklist
$blocklist
IP-addresses to ban anyhow (array of IP-address, optionally in CIDR notation).
Definition: Security.php:11
Rsi\Fred
Definition: Alive.php:3