nette/http — pull request #32 by JanTvrdik (2014-12-18 11:09:54+00:00)
Title: Url: optimized unescape() performance

Currently unescape() can be used to perform DOS on Nette application if HTTP server does not restrict URL length.

Benchmark

~~~php
$datasets = [
'a' => ['%66%6F%6F', '', 'foo'],
'b' => ['%66%6F%6F', 'o', 'f%6F%6F'],
'c' => ['%66%6F%6F', 'f', '%66oo'],
'd' => ['%66%6F%6F', 'fo', '%66%6F%6F'],
'e' => ['%66%6f%6f', 'fo', '%66%6f%6f'],
'no-escapes (a)' => [str_repeat('a', 1e6), '', str_repeat('a', 1e6)],
'no-escapes (z)' => [str_repeat('z', 1e6), '', str_repeat('z', 1e6)],
'escapes' => [str_repeat('%40', 1e5), '', str_repeat('@', 1e5)],
'escapes2' => [str_repeat('%40', 1e5 / 2) . '%20' . str_repeat('%40', 1e5 / 2), ' ', str_repeat('@', 1e5 / 2) . '%20' . str_repeat('@', 1e5 / 2)],
'escapes3' => [str_repeat('%40', 1e5), '@', str_repeat('%40', 1e5)],
];

$tests = [
'old' => function ($testCount, $s, $reserved, $expected) {
for ($i = 0; $i < $testCount; $i++) {
preg_match_all('#(?<=%)[a-f0-9][a-f0-9]#i', $s, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach (array_reverse($matches) as $match) {
$ch = chr(hexdec($match[0][0]));
if (strpos($reserved, $ch) === FALSE) {
$s = substr_replace($s, $ch, $match[0][1] - 1, 3);
}
}
}
Assert::same($expected, $s);
},
'new' => function ($testCount, $s, $reserved, $expected) {
for ($i = 0; $i < $testCount; $i++) {
preg_match_all('#%[a-f0-9][a-f0-9]#i', $s, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach (array_reverse($matches) as $match) {
$ch = chr(hexdec(substr($match[0][0], 1)));
if (strpos($reserved, $ch) === FALSE) {
$s = substr_replace($s, $ch, $match[0][1], 3);
}
}
}
Assert::same($expected, $s);
},
'split' => function ($testCount, $s, $reserved, $expected) {
for ($i = 0; $i < $testCount; $i++) {
if ($reserved === '') {
$s = rawurldecode($s);

} else {
$delimiters = array();
for ($i = 0; $i < strlen($reserved); $i++) {
$delimiters[] = strtoupper(dechex(ord($reserved[$i])));
}
$parts = preg_split('#(' . implode('|', $delimiters) . ')#i', $s, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i = 0; $i < count($parts); $i += 2) {
$parts[$i] = rawurldecode($parts[$i]);
}
$s = implode('', $parts);
}
}
Assert::same($expected, $s);
},
];

$testCount = 1;
$padLength = max(array_map('strlen', array_keys($datasets))) + 3;
foreach ($tests as $testName => $testCallback) {
printf("Test %s:\n", $testName);
foreach ($datasets as $datasetName => $dataset) {
$time = -microtime(TRUE);
$testCallback($testCount, ...$dataset);
$time += microtime(TRUE);
printf(" %s%5.0f ms\n", str_pad($datasetName, $padLength), $time * 1e3);
}
printf("\n");
}
~~~

Results:

~~~~
Test old:
a 0 ms
b 0 ms
c 0 ms
d 0 ms
e 0 ms
no-escapes (a) 37 ms
no-escapes (z) 33 ms
escapes 3303 ms
escapes2 3325 ms
escapes3 289 ms

Test new:
a 0 ms
b 0 ms
c 0 ms
d 0 ms
e 0 ms
no-escapes (a) 1 ms
no-escapes (z) 0 ms
escapes 3349 ms
escapes2 3461 ms
escapes3 277 ms

Test split:
a 0 ms
b 0 ms
c 0 ms
d 1 ms
e 0 ms
no-escapes (a) 1 ms
no-escapes (z) 1 ms
escapes 8 ms
escapes2 9 ms
escapes3 106 ms
~~~