Skip to content

Commit e7793ee

Browse files
committed
Added new firewall engine whitelist processor. Moved legacy whitelist rule processor to its own ruleset. Removed preg_replace to improve performance. Added test for new engine whitelist rules.
1 parent a6da8e3 commit e7793ee

6 files changed

Lines changed: 203 additions & 46 deletions

File tree

src/Extensions/WordPress/Extension.php

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,32 @@ class Extension implements ExtensionInterface
1212
* @var array
1313
*/
1414
public $options = array(
15-
'patchstack_basic_firewall_roles' => array('administrator', 'editor', 'author'),
16-
'patchstack_custom_whitelist_rules' => ''
15+
'patchstack_basic_firewall_roles' => array('administrator', 'editor', 'author'),
16+
'patchstack_custom_whitelist_rules' => ''
17+
);
18+
19+
/**
20+
* The request parameter values exploded into pairs.
21+
*
22+
* @var array
23+
*/
24+
private $requestParams = array(
25+
'method' => 'method',
26+
'rulesFile' => 'rules->file',
27+
'rulesRawPost' => 'rules->raw->post',
28+
'rulesUri' => 'rules->uri',
29+
'rulesHeadersAll' => 'rules->headers->all',
30+
'rulesHeadersKeys' => 'rules->headers->keys',
31+
'rulesHeadersValues' => 'rules->headers->values',
32+
'rulesHeadersCombinations' => 'rules->headers->combinations',
33+
'rulesBodyAll' => 'rules->body->all',
34+
'rulesBodyKeys' => 'rules->body->keys',
35+
'rulesBodyValues' => 'rules->body->values',
36+
'rulesBodyCombinations' => 'rules->body->combinations',
37+
'rulesParamsAll' => 'rules->params->all',
38+
'rulesParamsKeys' => 'rules->params->keys',
39+
'rulesParamsValues' => 'rules->params->values',
40+
'rulesParamsCombinations' => 'rules->params->combinations'
1741
);
1842

1943
/**
@@ -194,14 +218,12 @@ private function isWhitelistedCustom()
194218
if (count($t) == 2) {
195219
$val = strtolower(trim($t[1]));
196220
switch (strtolower($t[0])) {
197-
// IP address match.
198-
case 'ip':
221+
case 'ip': // IP address match.
199222
if ($ip == $val) {
200223
return true;
201224
}
202225
break;
203-
// Payload match.
204-
case 'payload':
226+
case 'payload': // Payload match.
205227
if (count($_POST) > 0 && strpos(strtolower(print_r($_POST, true)), $val) !== false) {
206228
return true;
207229
}
@@ -210,8 +232,7 @@ private function isWhitelistedCustom()
210232
return true;
211233
}
212234
break;
213-
// URL match.
214-
case 'url':
235+
case 'url': // URL match.
215236
if (strpos(strtolower($_SERVER['REQUEST_URI']), $val) !== false) {
216237
return true;
217238
}
@@ -225,6 +246,10 @@ private function isWhitelistedCustom()
225246

226247
/**
227248
* Determine if the request is whitelisted.
249+
*
250+
* @param array $whitelistRules
251+
* @param array $request
252+
* @return boolean
228253
*/
229254
public function isWhitelisted($whitelistRules, $request)
230255
{
@@ -239,28 +264,28 @@ public function isWhitelisted($whitelistRules, $request)
239264
}
240265

241266
// Grab visitor's IP address and request data.
242-
$client_ip = $this->getIpAddress();
267+
$clientIp = $this->getIpAddress();
243268
$requests = $request;
244269

245270
foreach ($whitelistRules as $whitelist) {
246-
$whitelist_rule = json_decode($whitelist['rule']);
271+
$whitelistRule = json_decode($whitelist['rule']);
247272

248273
// If an IP address match is given, determine if it matches.
249-
$ip = isset($whitelist_rule->rules, $whitelist_rule->rules->ip_address) ? $whitelist_rule->rules->ip_address : null;
274+
$ip = isset($whitelistRule->rules, $whitelistRule->rules->ip_address) ? $whitelistRule->rules->ip_address : null;
250275
if (!is_null($ip)) {
251276
if (strpos($ip, '*') !== false) {
252-
$whitelisted_ip = $this->plugin->ban->check_wildcard_rule($client_ip, $ip);
277+
$isWhitelistedIp = $this->plugin->ban->check_wildcard_rule($clientIp, $ip);
253278
} elseif (strpos($ip, '-') !== false) {
254-
$whitelisted_ip = $this->plugin->ban->check_range_rule($client_ip, $ip);
279+
$isWhitelistedIp = $this->plugin->ban->check_range_rule($clientIp, $ip);
255280
} elseif (strpos($ip, '/') !== false) {
256-
$whitelisted_ip = $this->plugin->ban->check_subnet_mask_rule($client_ip, $ip);
257-
} elseif ($client_ip == $ip) {
258-
$whitelisted_ip = true;
281+
$isWhitelistedIp = $this->plugin->ban->check_subnet_mask_rule($clientIp, $ip);
282+
} elseif ($clientIp == $ip) {
283+
$isWhitelistedIp = true;
259284
} else {
260-
$whitelisted_ip = false;
285+
$isWhitelistedIp = false;
261286
}
262287
} else {
263-
$whitelisted_ip = true;
288+
$isWhitelistedIp = true;
264289
}
265290

266291
foreach ($requests as $key => $request) {
@@ -269,21 +294,20 @@ public function isWhitelisted($whitelistRules, $request)
269294
$key = 'rulesBodyAll';
270295
}
271296

272-
if ($whitelist_rule->method == $requests['method'] || $whitelist_rule->method == 'ALL') {
273-
$test = strtolower(preg_replace('/(?!^)[A-Z]{2,}(?=[A-Z][a-z])|[A-Z][a-z]/', '->$0', $key));
274-
$exp = explode('->', $test);
297+
if (isset($this->requestParams[$key]) && ($whitelistRule->method == $requests['method'] || $whitelistRule->method == 'ALL')) {
298+
$exp = explode('->', $this->requestParams[$key]);
275299

276300
// Determine if a rule exists for this request.
277-
$rule = $whitelist_rule;
301+
$rule = $whitelistRule;
278302
foreach ($exp as $var) {
279303
if (!isset($rule->$var)) {
280-
$rule = null;
281-
continue;
304+
$rule = null;
305+
continue;
282306
}
283307
$rule = $rule->$var;
284308
}
285309

286-
if (!is_null($rule) && substr($key, 0, 4) == 'rule' && $this->isRuleMatch($rule, $request) && $whitelisted_ip) {
310+
if (!is_null($rule) && substr($key, 0, 4) == 'rule' && $this->isRuleMatch($rule, $request) && $isWhitelistedIp) {
287311
return true;
288312
}
289313
}

src/Processor.php

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class Processor
2929
*/
3030
private $whitelistRules = array();
3131

32+
/**
33+
* The legacy whitelist rules to process.
34+
*
35+
* @var array
36+
*/
37+
private $whitelistRulesLegacy = array();
38+
3239
/**
3340
* Firewall datasets which can be interacted with by the firewall rules.
3441
*
@@ -82,24 +89,29 @@ class Processor
8289
*
8390
* @param ExtensionInterface $extension
8491
* @param array $firewallRules
85-
* @param array $firewallRulesLegacy
8692
* @param array $whitelistRules
8793
* @param array $options
94+
* @param array $datasets
95+
* @param array $firewallRulesLegacy
96+
* @param array $whitelistRulesLegacy
97+
* @return void
8898
*/
8999
public function __construct(
90100
ExtensionInterface $extension,
91101
$firewallRules = array(),
92-
$firewallRulesLegacy = array(),
93102
$whitelistRules = array(),
94103
$options = array(),
95-
$datasets = array()
104+
$datasets = array(),
105+
$firewallRulesLegacy = array(),
106+
$whitelistRulesLegacy = array()
96107
) {
97108
$this->extension = $extension;
98109
$this->firewallRules = $firewallRules;
99-
$this->firewallRulesLegacy = $firewallRulesLegacy;
100110
$this->whitelistRules = $whitelistRules;
101111
$this->options = array_merge($this->options, $options);
102112
$this->dataset = $datasets;
113+
$this->firewallRulesLegacy = $firewallRulesLegacy;
114+
$this->whitelistRulesLegacy = $whitelistRulesLegacy;
103115

104116
$this->secret = isset($options['secret']) ? $options['secret'] : 'secret';
105117
$this->request = new Request($this->options);
@@ -109,6 +121,7 @@ public function __construct(
109121
/**
110122
* Magic getter for the options.
111123
*
124+
* @param string $name
112125
* @return mixed
113126
*/
114127
public function __get($name)
@@ -132,22 +145,14 @@ public function launch($mustExit = true)
132145
$this->extension->forceExit(22);
133146
}
134147

135-
// Check for whitelist.
148+
// Check for whitelist based on the legacy whitelist rules.
136149
$request = $this->request->capture();
137-
if ($this->extension->isWhitelisted($this->whitelistRules, $request)) {
150+
if ($this->extension->isWhitelisted($this->whitelistRulesLegacy, $request)) {
138151
return true;
139152
}
140153

141-
// Grab the IP address of the request.
142-
$ip = $this->extension->getIpAddress();
143-
144-
// Run the legacy firewall rules processor for backwards compatibility.
145-
if (count($this->firewallRulesLegacy) > 0) {
146-
$this->launchLegacy(true, $request, $ip);
147-
}
148-
149-
// Determine if we have any firewall rules loaded.
150-
if (count($this->firewallRules) == 0) {
154+
// Determine if we have any firewall and/or whitelist rules loaded.
155+
if (count($this->firewallRules) == 0 && count($this->whitelistRules) == 0) {
151156
return true;
152157
}
153158

@@ -159,9 +164,15 @@ public function launch($mustExit = true)
159164
\Laravel\SerializableClosure\SerializableClosure::setSecretKey($this->secret);
160165
}
161166

167+
// Grab the IP address of the request.
168+
$ip = $this->extension->getIpAddress();
169+
162170
// Store the datasets in a shorter variable for easy access.
163171
$dataset = $this->dataset;
164-
foreach ($this->firewallRules as $rule) {
172+
173+
// Merge the rules together. First iterate through the whitelist rules.
174+
$rules = array_merge($this->whitelistRules, $this->firewallRules);
175+
foreach ($rules as $rule) {
165176
// Get the firewall rule and extract it.
166177
$vpatch = base64_decode($rule->rule);
167178
if (!$vpatch) {
@@ -202,9 +213,16 @@ public function launch($mustExit = true)
202213
} elseif ($rule->type == 'REDIRECT') {
203214
$this->extension->logRequest($rule->id, $request, 'REDIRECT');
204215
$this->response->redirect($rule->type_params, $mustExit);
216+
} elseif ($rule->type == 'WHITELIST') {
217+
return $mustExit;
205218
}
206219
}
207220

221+
// Run the legacy firewall rules processor for backwards compatibility.
222+
if (count($this->firewallRulesLegacy) > 0) {
223+
$this->launchLegacy(true, $request, $ip);
224+
}
225+
208226
return true;
209227
}
210228

@@ -223,6 +241,26 @@ public function launchLegacy($mustExit = true, $request = array(), $ip = '')
223241
$client_ip = $ip == '' ? $this->extension->getIpAddress() : $ip;
224242
$requests = count($request) == 0 ? $this->request->capture() : $request;
225243

244+
// The request parameter values exploded into pairs.
245+
$requestParams = array(
246+
'method' => 'method',
247+
'rulesFile' => 'rules->file',
248+
'rulesRawPost' => 'rules->raw->post',
249+
'rulesUri' => 'rules->uri',
250+
'rulesHeadersAll' => 'rules->headers->all',
251+
'rulesHeadersKeys' => 'rules->headers->keys',
252+
'rulesHeadersValues' => 'rules->headers->values',
253+
'rulesHeadersCombinations' => 'rules->headers->combinations',
254+
'rulesBodyAll' => 'rules->body->all',
255+
'rulesBodyKeys' => 'rules->body->keys',
256+
'rulesBodyValues' => 'rules->body->values',
257+
'rulesBodyCombinations' => 'rules->body->combinations',
258+
'rulesParamsAll' => 'rules->params->all',
259+
'rulesParamsKeys' => 'rules->params->keys',
260+
'rulesParamsValues' => 'rules->params->values',
261+
'rulesParamsCombinations' => 'rules->params->combinations'
262+
);
263+
226264
// Iterate through all root objects.
227265
foreach ($this->firewallRulesLegacy as $firewall_rule) {
228266
$rule_terms = json_decode($firewall_rule['rule']);
@@ -255,8 +293,10 @@ public function launchLegacy($mustExit = true, $request = array(), $ip = '')
255293

256294
// Determine if the requesting method matches.
257295
if ($rule_terms->method == $requests['method'] || $rule_terms->method == 'ALL' || $rule_terms->method == 'GET' || ($rule_terms->method == 'FILES' && $this->extension->isFileUploadRequest())) {
258-
$test = strtolower(preg_replace('/(?!^)[A-Z]{2,}(?=[A-Z][a-z])|[A-Z][a-z]/', '->$0', $key));
259-
$exp = explode('->', $test);
296+
if (!isset($requestParams[$key])) {
297+
continue;
298+
}
299+
$exp = explode('->', $requestParams[$key]);
260300

261301
// Determine if a rule exists for this request.
262302
$rule = $rule_terms;
@@ -296,7 +336,7 @@ public function launchLegacy($mustExit = true, $request = array(), $ip = '')
296336
/**
297337
* Determine if the request matches the given firewall or whitelist rule.
298338
*
299-
* @param string $rule
339+
* @param string $rule
300340
* @param string|array $request
301341
* @return bool
302342
*/

tests/FirewallDatasetTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ private function setUpFirewallProcessor(array $rules)
4646
$rules,
4747
[],
4848
[],
49-
[],
5049
$this->datasets
5150
);
5251
}

tests/FirewallLegacyTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ private function setUpFirewallProcessor(array $rules)
3939
$this->processor = new Processor(
4040
new Extension(),
4141
[],
42+
[],
43+
[],
44+
[],
4245
$rules
4346
);
4447
}

0 commit comments

Comments
 (0)