Skip to content

Commit

Permalink
Use composer/pcre for better regexp static analysis (#416)
Browse files Browse the repository at this point in the history
Use composer/preg's Regex class instead of `preg_*()` and `Strings::*()`.
Also use the provided PHPStan extension to check regular expressions.

Close #401
  • Loading branch information
spaze authored Oct 20, 2024
2 parents e4c47e7 + a9a92be commit 00f510c
Show file tree
Hide file tree
Showing 43 changed files with 2,191 additions and 34 deletions.
1 change: 1 addition & 0 deletions app/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"ext-pdo": "*",
"ext-simplexml": "*",
"ext-sodium": "*",
"composer/pcre": "^3.3.1",
"contributte/translation": "^2.0",
"latte/latte": "^3.0.3",
"nette/application": "^3.1.10",
Expand Down
81 changes: 80 additions & 1 deletion app/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions app/disallowed-calls.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ parameters:
-
function: 'setcookie()'
message: 'use methods from MichalSpacekCz\Http\Cookies'
-
function: 'preg_*()'
message: 'use the Preg class from composer/pcre'
disallowedStaticCalls:
-
method: 'Tester\Environment::skip()'
message: 'use TestCaseRunner::skip() instead, it can ignore skipping with an environment variable'
allowInMethods:
- 'MichalSpacekCz\Test\TestCaseRunner::skip()'
-
method:
- 'Nette\Utils\Strings::match()'
- 'Nette\Utils\Strings::matchAll()'
- 'Nette\Utils\Strings::replace()'
message: 'use the Preg or Regex class from composer/pcre for better static analysis'
disallowedMethodCalls:
-
method:
Expand Down
13 changes: 13 additions & 0 deletions app/phpstan-vendor.neon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ parameters:
- vendor/slevomat/coding-standard/*
- vendor/squizlabs/php_codesniffer/*
# Throws "No error to ignore is reported on line", which can't be ignored, probably because only custom ruleset is used
- vendor/composer/pcre/src/Regex.php
- vendor/contributte/translation/src/DI/TranslationExtension.php
- vendor/contributte/translation/src/Latte/Macros.php
- vendor/metisfw/phpstan-nette-links/src/Nette/PresenterResolver.php
Expand All @@ -58,6 +59,10 @@ parameters:
- vendor/latte/latte/src/Tools/Linter.php
- vendor/nette/tester/src/Runner/CliTester.php
- vendor/nette/tester/src/Runner/Runner.php
-
function: 'preg_*()'
allowIn:
- vendor/*.php
# bundled disallowed-dangerous-calls.neon
-
function: 'eval()'
Expand Down Expand Up @@ -208,6 +213,14 @@ parameters:
message: 'use TestCaseRunner::skip() instead, it can ignore skipping with an environment variable'
allowIn:
- vendor/nette/tester/src/Framework/TestCase.php
-
method:
- 'Nette\Utils\Strings::match()'
- 'Nette\Utils\Strings::matchAll()'
- 'Nette\Utils\Strings::replace()'
allowIn:
- vendor/contributte/*.php
- vendor/nette/*.php
disallowedSuperglobals:
-
superglobal: '$_SERVER'
Expand Down
1 change: 1 addition & 0 deletions app/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ includes:
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
- vendor/metisfw/phpstan-nette-links/extension.neon
- vendor/metisfw/phpstan-nette-links/rules.neon
- vendor/composer/pcre/extension.neon
5 changes: 0 additions & 5 deletions app/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@
<code><![CDATA[$units->id]]></code>
</MixedArgument>
</file>
<file src="src/EasterEgg/CrLfUrlInjections.php">
<MixedArgument>
<code><![CDATA[$match[1]]]></code>
</MixedArgument>
</file>
<file src="src/EasterEgg/WinterIsComing.php">
<MixedArgument>
<code><![CDATA[$input->getValue()]]></code>
Expand Down
8 changes: 4 additions & 4 deletions app/src/EasterEgg/CrLfUrlInjections.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

namespace MichalSpacekCz\EasterEgg;

use Composer\Pcre\Regex;
use Nette\Http\IRequest;
use Nette\Http\IResponse;
use Nette\Utils\Strings;

readonly class CrLfUrlInjections
{
Expand All @@ -26,12 +26,12 @@ public function detectAttempt(): bool
if (!str_contains($url, "\r") && !str_contains($url, "\n")) {
return false;
}
$matches = Strings::matchAll($url, sprintf('/Set\-Cookie:%s=([a-z0-9]+)/i', self::COOKIE_NAME));
foreach ($matches as $match) {
$matches = Regex::matchAllStrictGroups(sprintf('/Set\-Cookie:%s=([a-z0-9]+)/i', self::COOKIE_NAME), $url);
foreach ($matches->matches[1] as $match) {
// Don't use any cookie name from the request to avoid e.g. session fixation
$this->httpResponse->setCookie(
self::COOKIE_NAME,
$match[1],
$match,
time() - 31337 * 1337,
'/expired=31337*1337seconds/(1.3years)/ago',
);
Expand Down
4 changes: 2 additions & 2 deletions app/src/EasterEgg/WinterIsComing.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

namespace MichalSpacekCz\EasterEgg;

use Composer\Pcre\Regex;
use MichalSpacekCz\ShouldNotHappenException;
use Nette\Application\Responses\TextResponse;
use Nette\Application\UI\Presenter;
use Nette\Forms\Controls\TextInput;
use Nette\Utils\Arrays;
use Nette\Utils\Strings;

class WinterIsComing
{
Expand Down Expand Up @@ -42,7 +42,7 @@ public function ruleEmail(): callable
is_string($input->getValue())
&& (
Arrays::contains(self::EMAILS, $input->getValue())
|| Strings::match($input->getValue(), '/@(' . implode('|', array_map('preg_quote', self::HOSTS)) . ')$/') !== null
|| Regex::isMatch('/@(' . implode('|', array_map('preg_quote', self::HOSTS)) . ')$/', $input->getValue())
)
) {
$this->sendSyntaxError($input);
Expand Down
5 changes: 3 additions & 2 deletions app/src/Form/SignInHoneypotFormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace MichalSpacekCz\Form;

use Composer\Pcre\Regex;
use MichalSpacekCz\Form\Controls\FormControlsFactory;
use Nette\Http\IRequest;
use Nette\Utils\Html;
Expand All @@ -27,7 +28,7 @@ public function create(): UiForm
$values = $form->getFormValues();
Debugger::log("Sign-in attempt: {$values->username}, {$values->password}, {$this->httpRequest->getRemoteAddress()}", 'honeypot');
$creds = $values->username . ':' . $values->password;
if (preg_match('~\slimit\s~i', $creds)) {
if (Regex::isMatch('~\slimit\s~i', $creds)) {
$message = Html::el()
->setText("No, no, no, no, no, no, no, no, no, no, no, no there's ")
->addHtml(Html::el('a')
Expand All @@ -37,7 +38,7 @@ public function create(): UiForm
->addText('!');
} elseif (stripos($creds, 'honeypot') !== false) {
$message = 'Jo jo, honeypot, přesně tak';
} elseif (preg_match('~\sor\s~i', $creds)) {
} elseif (Regex::isMatch('~\sor\s~i', $creds)) {
$message = 'Dobrej pokusql!';
} else {
$message = 'Špatné uživatelské jméno nebo heslo';
Expand Down
10 changes: 5 additions & 5 deletions app/src/Formatter/TexyFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

namespace MichalSpacekCz\Formatter;

use Composer\Pcre\Regex;
use Contributte\Translation\Exceptions\InvalidArgument;
use Contributte\Translation\Translator;
use MichalSpacekCz\Formatter\Placeholders\TexyFormatterPlaceholder;
use MichalSpacekCz\Utils\Hash;
use Nette\Utils\Html;
use Nette\Utils\Strings;
use Stringable;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
Expand Down Expand Up @@ -151,7 +151,7 @@ public function format(string $text): Html
{
$texy = $this->texy ?? $this->getTexy();
return $this->replace($text . self::CACHE_KEY_DELIMITER . __FUNCTION__, $texy, function () use ($texy, $text): string {
return Strings::replace($texy->process($text), '~^\s*<p[^>]*>(.*)</p>\s*$~s', '$1');
return Regex::replace('~^\s*<p[^>]*>(.*)</p>\s*$~s', '$1', $texy->process($text))->result;
});
}

Expand Down Expand Up @@ -188,13 +188,13 @@ private function replace(string $key, Texy $texy, callable $callback): Html
$replacements[$placeholder::getId()] = $placeholder->replace(...);
}

$result = Strings::replace(
$result,
$result = Regex::replaceCallbackStrictGroups(
'~\*\*([^:]+):([^*]+)\*\*~',
function (array $matches) use ($replacements): string {
return (isset($replacements[$matches[1]]) ? $replacements[$matches[1]]($matches[2]) : '');
},
);
$result,
)->result;
return Html::el()->setHtml($result);
}

Expand Down
15 changes: 11 additions & 4 deletions app/src/Formatter/TexyPhraseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace MichalSpacekCz\Formatter;

use Composer\Pcre\Regex;
use Contributte\Translation\Translator;
use MichalSpacekCz\Application\Locale\LocaleLinkGenerator;
use MichalSpacekCz\Articles\Blog\BlogPostLocaleUrls;
Expand Down Expand Up @@ -61,8 +62,11 @@ public function solve(HandlerInvocation $invocation, string $phrase, string $con
}

// "title":[link-en_US:Module:Presenter:action params]
if (str_starts_with($url, 'link-') && preg_match("/^link-{$localeRegExp}:(.*)\\z/", $url, $matches)) {
$link->URL = $this->getLink($matches[2], $matches[1]);
if (str_starts_with($url, 'link-')) {
$result = Regex::matchStrictGroups("/^link-{$localeRegExp}:(.*)\\z/", $url);
if ($result->matched) {
$link->URL = $this->getLink($result->matches[2], $result->matches[1]);
}
}

// "title":[blog:post#fragment]
Expand All @@ -71,8 +75,11 @@ public function solve(HandlerInvocation $invocation, string $phrase, string $con
}

// "title":[blog-en_US:post#fragment]
if (str_starts_with($url, 'blog-') && preg_match("/^blog-{$localeRegExp}:(.*)\\z/", $url, $matches)) {
$link->URL = $this->getBlogLink($matches[2], $matches[1]);
if (str_starts_with($url, 'blog-')) {
$result = Regex::matchStrictGroups("/^blog-{$localeRegExp}:(.*)\\z/", $url);
if ($result->matched) {
$link->URL = $this->getBlogLink($result->matches[2], $result->matches[1]);
}
}

// "title":[inhouse-training:training]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace MichalSpacekCz\Training\ApplicationForm;

use Composer\Pcre\Regex;
use MichalSpacekCz\Training\Exceptions\SpammyApplicationException;
use stdClass;

Expand All @@ -17,7 +18,7 @@ class TrainingApplicationFormSpam

public function check(stdClass $values): void
{
if (preg_match('~\s+href="\s*https?://~', $values->note ?? '')) {
if (Regex::isMatch('~\s+href="\s*https?://~', $values->note ?? '')) {
throw new SpammyApplicationException();
} elseif (
ctype_lower($values->name ?? self::FIELD_MISSING_VALUE)
Expand Down
7 changes: 4 additions & 3 deletions app/src/Training/Applications/TrainingApplicationSources.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace MichalSpacekCz\Training\Applications;

use Composer\Pcre\Regex;
use MichalSpacekCz\Database\TypedDatabase;
use MichalSpacekCz\Training\Resolver\Vrana;
use Nette\Utils\Strings;
Expand Down Expand Up @@ -60,9 +61,9 @@ public function getDefaultSource(): string
*/
public function getSourceNameInitials(string $name): string
{
$name = Strings::replace($name, '/,? s\.r\.o./', '');
$matches = Strings::matchAll($name, '/(?<=\s|\b)\pL/u', PREG_PATTERN_ORDER);
return Strings::upper(implode('', current($matches)));
$replace = Regex::replace('/,? s\.r\.o./', '', $name);
$matches = Regex::matchAll('/(?<=\s|\b)\pL/u', $replace->result);
return Strings::upper(implode('', $matches->matches[0]));
}

}
9 changes: 5 additions & 4 deletions app/src/UpcKeys/Technicolor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace MichalSpacekCz\UpcKeys;

use Composer\Pcre\Regex;
use DateTime;
use MichalSpacekCz\Http\Client\HttpClient;
use MichalSpacekCz\Http\Client\HttpClientRequest;
Expand Down Expand Up @@ -95,10 +96,11 @@ private function generateKeys(string $ssid): array
continue;
}

if (!preg_match('/([^,]+),([^,]+),(\d+)/', $line, $matches)) {
$result = Regex::matchStrictGroups('/([^,]+),([^,]+),(\d+)/', $line);
if (!$result->matched) {
throw new UpcKeysApiIncorrectTokensException($json, $line);
}
[, $serial, $key, $type] = $matches;
[, $serial, $key, $type] = $result->matches;
$keys["{$type}-{$serial}"] = $this->buildKey($serial, $key, (int)$type);
}
ksort($keys);
Expand Down Expand Up @@ -186,8 +188,7 @@ private function storeKeys(string $ssid, array $keys): void
*/
private function buildKey(string $serial, string $key, int $type): WiFiKey
{
preg_match('/^[a-z]+/i', $serial, $matches);
$prefix = current($matches);
$prefix = Regex::match('/^[a-z]+/i', $serial)->matches[0] ?? false;
if ($prefix === false || !in_array($prefix, self::PREFIXES)) {
throw new UpcKeysApiUnknownPrefixException($serial);
}
Expand Down
Loading

0 comments on commit 00f510c

Please sign in to comment.