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