Skip to content

Commit 2363cb9

Browse files
committed
Added: ability to match against wildcards in parameter name.
Added: test for ctype_special and wildcard parameter name matching.
1 parent 674aab7 commit 2363cb9

4 files changed

Lines changed: 171 additions & 33 deletions

File tree

src/Processor.php

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,14 @@ public function launch($mustExit = true)
147147
return true;
148148
}
149149

150-
// Determine if the current request is whitelisted or not (role based).
150+
// Determine if the current request is whitelisted or not.
151151
$isWhitelisted = !$this->mustUsePluginCall && $this->extension->canBypass();
152152

153-
// Merge the rules together. First iterate through the whitelist rules.
153+
// Merge the rules together. First iterate through the whitelist rules because
154+
// we want to whitelist the request if there's a whitelist rule match.
154155
$rules = array_merge($this->whitelistRules, $this->firewallRules);
156+
157+
// Iterate through all the firewall rules.
155158
foreach ($rules as $rule) {
156159
// Should never happen.
157160
if (!isset($rule['rules']) || empty($rule['rules'])) {
@@ -230,36 +233,44 @@ public function executeFirewall($rules)
230233
}
231234

232235
// Extract the value of the paramater that we want.
233-
$value = $this->request->getParameterValue($rule['parameter']);
234-
if (is_null($value) && $rule['parameter'] !== false && $rule['parameter'] != 'rules') {
236+
$values = $this->request->getParameterValues($rule['parameter']);
237+
if (is_null($values) && $rule['parameter'] !== false && $rule['parameter'] != 'rules') {
235238
continue;
236239
}
237240

238-
// Apply mutations, if any.
239-
if (isset($rule['mutations']) && is_array($rule['mutations'])) {
240-
$value = $this->request->applyMutation($rule['mutations'], $value);
241-
if (is_null($value)) {
242-
continue;
243-
}
241+
// For special parameter values we just set the array to a single null value.
242+
if ($rule['parameter'] === false || $rule['parameter'] == 'rules') {
243+
$values = [null];
244244
}
245245

246-
// Perform the matching.
247-
if (isset($rule['match']) && is_array($rule['match']) || isset($rule['rules'])) {
248-
249-
// Do we have to process child-rules?
250-
if (isset($rule['rules'])) {
251-
$match = $this->executeFirewall($rule['rules']);
252-
} else {
253-
$match = $this->matchParameterValue($rule['match'], $value);
246+
// For all field matches, we want to execute the rule against it.
247+
foreach ($values as $value) {
248+
// Apply mutations, if any.
249+
if (isset($rule['mutations']) && is_array($rule['mutations'])) {
250+
$value = $this->request->applyMutation($rule['mutations'], $value);
251+
if (is_null($value)) {
252+
continue;
253+
}
254254
}
255255

256-
// Is the rule a match?
257-
if ($match) {
258-
// In case there are multiple rules, they may require chained AND conditions.
259-
if ($inclusiveCount <= 1 || !isset($rule['inclusive']) || $rule['inclusive'] !== true) {
260-
return true;
256+
// Perform the matching.
257+
if (isset($rule['match']) && is_array($rule['match']) || isset($rule['rules'])) {
258+
259+
// Do we have to process child-rules?
260+
if (isset($rule['rules'])) {
261+
$match = $this->executeFirewall($rule['rules']);
261262
} else {
262-
$inclusiveHits++;
263+
$match = $this->matchParameterValue($rule['match'], $value);
264+
}
265+
266+
// Is the rule a match?
267+
if ($match) {
268+
// In case there are multiple rules, they may require chained AND conditions.
269+
if ($inclusiveCount <= 1 || !isset($rule['inclusive']) || $rule['inclusive'] !== true) {
270+
return true;
271+
} else {
272+
$inclusiveHits++;
273+
}
263274
}
264275
}
265276
}
@@ -387,8 +398,13 @@ public function matchParameterValue($match, $value)
387398

388399
// If a specific parameter key matches a sub-match condition.
389400
if ($matchType == 'array_key_value' && isset($match['key'], $match['match'])) {
390-
$value = $this->request->getParameterValue($match['key'], $value);
391-
return $this->matchParameterValue($match['match'], $value);
401+
$values = $this->request->getParameterValues($match['key'], $value);
402+
foreach ($values as $value) {
403+
if ($this->matchParameterValue($match['match'], $value)) {
404+
return true;
405+
}
406+
}
407+
return false;
392408
}
393409

394410
// If the user provided value does not match the current hostname.

src/Request.php

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ public function __construct($options, ExtensionInterface $extension)
3737
*
3838
* @param mixed $parameter
3939
* @param array $data
40-
* @return mixed|null
40+
* @return mixed
4141
*/
42-
public function getParameterValue($parameter, $data = [])
42+
public function getParameterValues($parameter, $data = [])
4343
{
4444
// For when a rule contains sub-rules.
4545
if (empty($parameter) || ctype_digit($parameter)) {
@@ -60,7 +60,7 @@ public function getParameterValue($parameter, $data = [])
6060
'post' => $_POST,
6161
'get' => $_GET,
6262
'url' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '',
63-
'raw' => ['raw' => $this->getParameterValue('raw')]
63+
'raw' => ['raw' => $this->getParameterValues('raw')]
6464
];
6565
break;
6666
case 'post':
@@ -109,7 +109,7 @@ public function getParameterValue($parameter, $data = [])
109109

110110
// If it's not an array, no need to continue.
111111
if (!is_array($data)) {
112-
return $data;
112+
return [$data];
113113
}
114114
default:
115115
break;
@@ -122,12 +122,20 @@ public function getParameterValue($parameter, $data = [])
122122

123123
// Special condition for the IP address.
124124
if ($type === 'server' && $t[0] === 'ip') {
125-
return $this->extension->getIpAddress();
125+
return [$this->extension->getIpAddress()];
126+
}
127+
128+
// For wildcard matching we handle it a bit differently.
129+
// We want to extract all wildcard matches and pass them as an array so we
130+
// can execute a firewall rule against all the fields that match.
131+
if (strpos($parameter, '*') !== false) {
132+
$values = $this->getValuesByWildcard($data, $parameter);
133+
return count($values) == 0 ? null : $values;
126134
}
127135

128136
// Just one parameter we have to match against.
129137
if (count($t) === 1) {
130-
return isset($data[$t[0]]) ? $data[$t[0]] : null;
138+
return isset($data[$t[0]]) ? [$data[$t[0]]] : null;
131139
}
132140

133141
// For multidimensional arrays.
@@ -141,7 +149,7 @@ public function getParameterValue($parameter, $data = [])
141149
$end = $end[ $var ];
142150
}
143151

144-
return $skip ? null : $end;
152+
return $skip ? null : [$end];
145153
}
146154

147155
/**
@@ -219,6 +227,52 @@ public function applyMutation($mutations, $value)
219227
return $value;
220228
}
221229

230+
/**
231+
* Given an array, get all parameters which match a certain wildcard.
232+
*
233+
* @param array $data
234+
* @param string $parameter
235+
* @return array
236+
*/
237+
public function getValuesByWildcard($data, $parameter)
238+
{
239+
// First we want to get the furthest possible down.
240+
$t = explode('.', $parameter);
241+
array_shift($t);
242+
$end = $data;
243+
$wildcard = '';
244+
foreach ( $t as $var ) {
245+
246+
// We hit the wildcard.
247+
if (strpos($var, '*') !== false) {
248+
$wildcard = str_replace('*', '', $var);
249+
break;
250+
}
251+
252+
// We're not at the end and there's no wildcard.
253+
if (!isset( $end[ $var ] ) && strpos($var, '*') === false) {
254+
return [];
255+
}
256+
257+
$end = $end[ $var ];
258+
}
259+
260+
// No need to continue if there is nothing to match.
261+
if (!is_array($end) || count($end) == 0) {
262+
return [];
263+
}
264+
265+
// Based on the data that is left, find the wildcard matches.
266+
$return = [];
267+
foreach ($end as $key => $value) {
268+
if (stripos($key, $wildcard) !== false) {
269+
$return[] = $value;
270+
}
271+
}
272+
273+
return $return;
274+
}
275+
222276
/**
223277
* Given an array, multi-dimensional or not, extract all of its values.
224278
*

tests/FirewallTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,57 @@ public function testRules()
245245
);
246246
$this->assertFalse($this->processor->launch(false));
247247
$this->alterPayload();
248+
249+
// Determine if a POST parameter is not a ctype_alnum.
250+
$this->setUpFirewallProcessor([$this->rules[17]]);
251+
$this->alterPayload(
252+
['POST' => [
253+
'value' => 'something_-0-9 '
254+
]]
255+
);
256+
$this->assertTrue($this->processor->launch(false));
257+
$this->alterPayload();
258+
259+
$this->setUpFirewallProcessor([$this->rules[17]]);
260+
$this->alterPayload(
261+
['POST' => [
262+
'value' => 'sleep(5), id'
263+
]]
264+
);
265+
$this->assertFalse($this->processor->launch(false));
266+
$this->alterPayload();
267+
//post.user.role.type*
268+
// Determine if a POST parameter (using wildcard) contains a certain character.
269+
$this->setUpFirewallProcessor([$this->rules[18]]);
270+
$this->alterPayload(
271+
['POST' => [
272+
'user' => [
273+
'test' => 'test',
274+
'role' => [
275+
'test' => 'test',
276+
'type1' => 'subscriber',
277+
'type2' => 'editor'
278+
]
279+
]
280+
]]
281+
);
282+
$this->assertTrue($this->processor->launch(false));
283+
$this->alterPayload();
284+
285+
$this->setUpFirewallProcessor([$this->rules[18]]);
286+
$this->alterPayload(
287+
['POST' => [
288+
'user' => [
289+
'test' => 'test',
290+
'role' => [
291+
'test' => 'test',
292+
'type1' => 'editor',
293+
'type5' => 'administrator'
294+
]
295+
]
296+
]]
297+
);
298+
$this->assertFalse($this->processor->launch(false));
299+
$this->alterPayload();
248300
}
249301
}

tests/data/Rules.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,21 @@
134134
"cat":"TEST",
135135
"type":"BLOCK",
136136
"type_params":null
137+
},
138+
{
139+
"id":18,
140+
"title":"Determine if a POST parameter is a ctype_special.",
141+
"rules":[{"parameter":"post.value","match":{"type":"ctype_special","value":false}}],
142+
"cat":"TEST",
143+
"type":"BLOCK",
144+
"type_params":null
145+
},
146+
{
147+
"id":19,
148+
"title":"Determine if a POST parameter (using wildcard) contains a certain character.",
149+
"rules":[{"parameter":"post.user.role.type*","match":{"type":"contains","value":"administrator"}}],
150+
"cat":"TEST",
151+
"type":"BLOCK",
152+
"type_params":null
137153
}
138154
]

0 commit comments

Comments
 (0)