From e3d0d14a137f9a07a5742f82e77b651b34879bce Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Fri, 10 Jan 2020 15:22:55 +0100
Subject: [PATCH] [TASK] Move VerifyDenyPattern functionality into separate
 logic

This change targets a couple of things:
- The global constant "FILE_DENY_PATTERN_DEFAULT" is moved to a class constant
- The global constant "PHP_EXTENSIONS_DEFAULT" which is not in use anymore, is removed.
- The security aspect of checking against the fileDenyPattern is extracted into its own
Class where
- The fileDenyPattern can never be empty, but only be set via DefaultConfiguration.

This makes it easier to test this functionality, a single object is taking over the responsibility, and the logic is now in one place. Also, the non-usage of the global constant makes life easier.

Resolves: #90147
Releases: master
Change-Id: I9db0d6fc3b10f75a3735017cb9ac0d9bfd4ff02b
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/62843
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
---
 .../Classes/Core/SystemEnvironmentBuilder.php |  15 +-
 .../core/Classes/Resource/ResourceStorage.php |   3 +-
 .../Resource/Security/FileNameValidator.php   |  86 ++++++
 .../TypoScript/Parser/TypoScriptParser.php    |   7 +-
 .../core/Classes/Utility/GeneralUtility.php   |   9 +-
 .../Configuration/DefaultConfiguration.php    |   1 -
 ...ecation-90147-UnifiedFileNameValidator.rst |  60 +++++
 .../Security/FileNameValidatorTest.php        | 255 ++++++++++++++++++
 .../Tests/Unit/Utility/GeneralUtilityTest.php | 131 ---------
 .../Utility/GeneralUtilityTest.php            | 131 +++++++++
 .../File/CreateFolderController.php           |   6 +-
 .../UploadedFileReferenceConverter.php        |   3 +-
 typo3/sysext/impexp/Classes/Import.php        |   3 +-
 typo3/sysext/impexp/Classes/ImportExport.php  |   3 +-
 .../ExtensionScanner/Php/ConstantMatcher.php  |  10 +
 .../Php/MethodCallStaticMatcher.php           |   7 +
 .../Classes/View/FolderUtilityRenderer.php    |   6 +-
 .../Classes/Report/Status/SecurityStatus.php  |  14 +-
 18 files changed, 590 insertions(+), 160 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Resource/Security/FileNameValidator.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-90147-UnifiedFileNameValidator.rst
 create mode 100644 typo3/sysext/core/Tests/Unit/Resource/Security/FileNameValidatorTest.php

diff --git a/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php b/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php
index b9096d18f7e6..18402f0f63ca 100644
--- a/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php
+++ b/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Core\Core;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 
@@ -92,6 +93,7 @@ class SystemEnvironmentBuilder
     {
         // Check one of the constants and return early if already defined,
         // needed if multiple requests are handled in one process, for instance in functional testing.
+        // This check can be removed in TYPO3 v11.0.
         if (defined('FILE_DENY_PATTERN_DEFAULT')) {
             return;
         }
@@ -101,16 +103,19 @@ class SystemEnvironmentBuilder
         defined('CR') ?: define('CR', chr(13));
         defined('CRLF') ?: define('CRLF', CR . LF);
 
-        // Security related constant: Default value of fileDenyPattern
-        define('FILE_DENY_PATTERN_DEFAULT', '\\.(php[3-8]?|phpsh|phtml|pht|phar|shtml|cgi)(\\..*)?$|\\.pl$|^\\.htaccess$');
-        // Security related constant: List of file extensions that should be registered as php script file extensions
-        define('PHP_EXTENSIONS_DEFAULT', 'php,php3,php4,php5,php6,php7,php8,phpsh,inc,phtml,pht,phar');
-
         // Relative path from document root to typo3/ directory, hardcoded to "typo3/"
         if (!defined('TYPO3_mainDir')) {
             define('TYPO3_mainDir', 'typo3/');
         }
 
+        /**
+         * @deprecated use FileNameAccess class to retrieve this information, will be removed in TYPO3 v11.0
+         */
+        define('FILE_DENY_PATTERN_DEFAULT', FileNameValidator::DEFAULT_FILE_DENY_PATTERN);
+        /**
+         * @deprecated use FILE_DENY_PATTERN and FileNameAccess class to retrieve this information, will be removed in TYPO3 v11.0
+         */
+        define('PHP_EXTENSIONS_DEFAULT', 'php,php3,php4,php5,php6,php7,php8,phpsh,inc,phtml,pht,phar');
         /**
          * @deprecated use Typo3Information class to retrieve this information, will be removed in TYPO3 v11.0
          */
diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
index 3d8c85d6e5fe..cb5bafb0f7e8 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
@@ -61,6 +61,7 @@ use TYPO3\CMS\Core\Resource\Search\Result\DriverFilteredSearchResult;
 use TYPO3\CMS\Core\Resource\Search\Result\EmptyFileSearchResult;
 use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResult;
 use TYPO3\CMS\Core\Resource\Search\Result\FileSearchResultInterface;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
@@ -832,7 +833,7 @@ class ResourceStorage implements ResourceStorageInterface
     protected function checkFileExtensionPermission($fileName)
     {
         $fileName = $this->driver->sanitizeFileName($fileName);
-        return GeneralUtility::verifyFilenameAgainstDenyPattern($fileName);
+        return GeneralUtility::makeInstance(FileNameValidator::class)->isValid($fileName);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Resource/Security/FileNameValidator.php b/typo3/sysext/core/Classes/Resource/Security/FileNameValidator.php
new file mode 100644
index 000000000000..7e5bf3083856
--- /dev/null
+++ b/typo3/sysext/core/Classes/Resource/Security/FileNameValidator.php
@@ -0,0 +1,86 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Resource\Security;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Ensures that any filename that an editor chooses for naming (or uses for uploading a file) is valid, meaning
+ * that no invalid characters (null-bytes) are added, or that the file does not contain an invalid file extension.
+ */
+class FileNameValidator
+{
+    /**
+     * Previously this was used within SystemEnvironmentBuilder
+     */
+    public const DEFAULT_FILE_DENY_PATTERN = '\\.(php[3-8]?|phpsh|phtml|pht|phar|shtml|cgi)(\\..*)?$|\\.pl$|^\\.htaccess$';
+
+    /**
+     * @var string
+     */
+    protected $fileDenyPattern;
+
+    public function __construct(string $fileDenyPattern = null)
+    {
+        if ($fileDenyPattern !== null) {
+            $this->fileDenyPattern = $fileDenyPattern;
+        } elseif (isset($GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'])) {
+            $this->fileDenyPattern = (string)$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'];
+        } else {
+            $this->fileDenyPattern = static::DEFAULT_FILE_DENY_PATTERN;
+        }
+    }
+
+    /**
+     * Verifies the input filename against the 'fileDenyPattern'
+     *
+     * Filenames are not allowed to contain control characters. Therefore we
+     * always filter on [[:cntrl:]].
+     *
+     * @param string $fileName File path to evaluate
+     * @return bool Returns TRUE if the file name is OK.
+     */
+    public function isValid(string $fileName): bool
+    {
+        $pattern = '/[[:cntrl:]]/';
+        if ($fileName !== '' && $this->fileDenyPattern !== '') {
+            $pattern = '/(?:[[:cntrl:]]|' . $this->fileDenyPattern . ')/iu';
+        }
+        return preg_match($pattern, $fileName) === 0;
+    }
+
+    /**
+     * Find out if there is a custom file deny pattern configured.
+     *
+     * @return bool
+     */
+    public function customFileDenyPatternConfigured(): bool
+    {
+        return $this->fileDenyPattern !== self::DEFAULT_FILE_DENY_PATTERN;
+    }
+
+    /**
+     * Checks if the given file deny pattern does not have parts that the default pattern should
+     * recommend. Used in status overview.
+     *
+     * @return bool
+     */
+    public function missingImportantPatterns(): bool
+    {
+        $defaultParts = explode('|', self::DEFAULT_FILE_DENY_PATTERN);
+        $givenParts = explode('|', $this->fileDenyPattern);
+        $missingParts = array_diff($defaultParts, $givenParts);
+        return !empty($missingParts);
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
index b466df66b135..84328d665a7d 100644
--- a/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
+++ b/typo3/sysext/core/Classes/TypoScript/Parser/TypoScriptParser.php
@@ -20,6 +20,7 @@ use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatche
 use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\TypoScript\ExtendedTemplateService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -1034,7 +1035,7 @@ class TypoScriptParser
             $readableFileName = rtrim($readableFilePrefix, '/') . '/' . $fileObject->getFilename();
             $content .= LF . '### @import \'' . $readableFileName . '\' begin ###' . LF;
             // Check for allowed files
-            if (!GeneralUtility::verifyFilenameAgainstDenyPattern($fileObject->getFilename())) {
+            if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($fileObject->getFilename())) {
                 $content .= self::typoscriptIncludeError('File "' . $readableFileName . '" was not included since it is not allowed due to fileDenyPattern.');
             } else {
                 $includedFiles[] = $fileObject->getPathname();
@@ -1105,7 +1106,7 @@ class TypoScriptParser
         if ((string)$filename !== '') {
             // Must exist and must not contain '..' and must be relative
             // Check for allowed files
-            if (!GeneralUtility::verifyFilenameAgainstDenyPattern($absfilename)) {
+            if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($absfilename)) {
                 $newString .= self::typoscriptIncludeError('File "' . $filename . '" was not included since it is not allowed due to fileDenyPattern.');
             } else {
                 $fileExists = false;
@@ -1294,7 +1295,7 @@ class TypoScriptParser
 
                     if ($inIncludePart === 'FILE') {
                         // Some file checks
-                        if (!GeneralUtility::verifyFilenameAgainstDenyPattern($realFileName)) {
+                        if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($realFileName)) {
                             throw new \UnexpectedValueException(sprintf('File "%s" was not included since it is not allowed due to fileDenyPattern.', $fileName), 1382651858);
                         }
                         if (empty($realFileName)) {
diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
index 3603d0f8657e..b61e43a10b5c 100644
--- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php
+++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Core\ClassLoadingInformation;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Http\RequestFactory;
 use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Service\OpcodeCacheService;
 use TYPO3\CMS\Core\SingletonInterface;
 
@@ -3060,14 +3061,12 @@ class GeneralUtility
      *
      * @param string $filename File path to evaluate
      * @return bool
+     * @deprecated will be removed in TYPO3 v11.0. Use the new FileNameValidator API instead.
      */
     public static function verifyFilenameAgainstDenyPattern($filename)
     {
-        $pattern = '/[[:cntrl:]]/';
-        if ((string)$filename !== '' && (string)$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] !== '') {
-            $pattern = '/(?:[[:cntrl:]]|' . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] . ')/iu';
-        }
-        return preg_match($pattern, $filename) === 0;
+        trigger_error('GeneralUtility::verifyFilenameAgainstDenyPattern() will be removed in TYPO3 v11.0. Use FileNameValidator->isValid($filename) instead.', E_USER_DEPRECATED);
+        return self::makeInstance(FileNameValidator::class)->isValid((string)$filename);
     }
 
     /**
diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php
index 02b12b12e7eb..27a861f8ecda 100644
--- a/typo3/sysext/core/Configuration/DefaultConfiguration.php
+++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php
@@ -1277,7 +1277,6 @@ return [
         'defaultPermissions' => [],
         'defaultUC' => [],
         'customPermOptions' => [], // Array with sets of custom permission options. Syntax is; 'key' => array('header' => 'header string, language split', 'items' => array('key' => array('label, language split','icon reference', 'Description text, language split'))). Keys cannot contain ":|," characters.
-        'fileDenyPattern' => FILE_DENY_PATTERN_DEFAULT,
         'interfaces' => 'backend',
         'explicitADmode' => 'explicitDeny',
         'flexformForceCDATA' => 0,
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90147-UnifiedFileNameValidator.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90147-UnifiedFileNameValidator.rst
new file mode 100644
index 000000000000..9d4233f1fbdb
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90147-UnifiedFileNameValidator.rst
@@ -0,0 +1,60 @@
+.. include:: ../../Includes.txt
+
+=================================================
+Deprecation: #90147 - Unified File Name Validator
+=================================================
+
+See :issue:`90147`
+
+Description
+===========
+
+The logic for validating if a new (uploaded) or renamed file's name is allowed, is now available in an encapsulated PHP class :php:`FileNameValidator`.
+
+The functionality is moved so all logic is encapsulated in one single place:
+- PHP constant `FILE_DENY_PATTERN_DEFAULT` is migrated into a class constant.
+- LocalConfiguration setting is only used when it differs from the default.
+- The GeneralUtility method is deprecated and calls `FileNameValidator->isValid()` directly.
+
+This optimization helps to only utilize and use PHP's memory if
+needed, and avoids to define run-time constants or variables,
+but only initializes the logic when needed - e.g. when uploading files or using TYPO3's importer via EXT:impexp.
+
+In addition, the PHP constant :php:`PHP_EXTENSIONS_DEFAULT` which is not
+in use anymore, is marked as deprecated as well.
+
+
+Impact
+======
+
+Using the method :php:`GeneralUtility::verifyFilenameAgainstDenyPattern()` directly will trigger a deprecation message.
+
+Using the constants will continue to work but will be removed TYPO3 v11.0.
+
+The system-wide setting to override the default file deny pattern, called :php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern']` is only set when different from the systems' default. If it is the same, the option is not set anymore by TYPO3 Core.
+
+
+Affected Installations
+======================
+
+TYPO3 installations with PHP code calling the mentioned method directly or using one of the global constants directly.
+
+
+Migration
+=========
+
+Instead of calling
+
+:php:`GeneralUtility::verifyFilenameAgainstDenyPattern($filename)`
+
+use
+
+:php:`GeneralUtility::makeInstance(FileNameValidator::class)->isValid($filename);`
+
+Instead of using the constant :php:`FILE_DENY_PATTERN_DEFAULT` use :php:`FileNameValidator::DEFAULT_FILE_DENY_PATTERN`.
+
+For the PHP constant :php:`PHP_EXTENSIONS_DEFAULT` there is no replacement, as it has no benefit for TYPO3 Core anymore.
+
+The extension scanner will detect the method calls or the usage of the constants.
+
+.. index:: LocalConfiguration, PHP-API, FullyScanned, ext:core
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Unit/Resource/Security/FileNameValidatorTest.php b/typo3/sysext/core/Tests/Unit/Resource/Security/FileNameValidatorTest.php
new file mode 100644
index 000000000000..b7c110668010
--- /dev/null
+++ b/typo3/sysext/core/Tests/Unit/Resource/Security/FileNameValidatorTest.php
@@ -0,0 +1,255 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Resource\Security;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use PHPUnit\Framework\TestCase;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
+use TYPO3\CMS\Core\Utility\StringUtility;
+
+class FileNameValidatorTest extends TestCase
+{
+    /**
+     * @return array
+     */
+    public function deniedFilesWithoutDenyPatternDataProvider(): array
+    {
+        return [
+            'Nul character in file' => ['image' . "\0" . '.gif'],
+            'Nul character in file with .php' => ['image.php' . "\0" . '.gif'],
+            'Nul character and UTF-8 in file' => ['Ссылка' . "\0" . '.gif'],
+            'Nul character and Latin-1 in file' => ['ÉÐØ' . "\0" . '.gif'],
+        ];
+    }
+
+    /**
+     * Tests whether validator detects files with nul character without file deny pattern.
+     *
+     * @param string $deniedFile
+     * @test
+     * @dataProvider deniedFilesWithoutDenyPatternDataProvider
+     */
+    public function verifyNulCharacterFilesAgainstPatternWithoutFileDenyPattern(string $deniedFile): void
+    {
+        $subject = new FileNameValidator('');
+        self::assertFalse($subject->isValid($deniedFile));
+
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] = '';
+        $subject = new FileNameValidator();
+        self::assertFalse($subject->isValid($deniedFile));
+    }
+
+    /**
+     * @return array
+     */
+    public function deniedFilesWithDefaultDenyPatternDataProvider(): array
+    {
+        $data = [
+            'Nul character in file' => ['image' . "\0", '.gif'],
+            'Nul character in file with .php' => ['image.php' . "\0", '.gif'],
+            'Nul character and UTF-8 in file' => ['Ссылка' . "\0", '.gif'],
+            'Nul character and Latin-1 in file' => ['ÉÐØ' . "\0", '.gif'],
+            'Lower umlaut .php file' => ['üWithFile', '.php'],
+            'Upper umlaut .php file' => ['fileWithÜ', '.php'],
+            'invalid UTF-8-sequence' => ["\xc0" . 'file', '.php'],
+            'Could be overlong NUL in some UTF-8 implementations, invalid in RFC3629' => ["\xc0\x80" . 'file', '.php'],
+            'Regular .php file' => ['file', '.php'],
+            'Regular .php3 file' => ['file', '.php3'],
+            'Regular .php5 file' => ['file', '.php5'],
+            'Regular .php7 file' => ['file', '.php7'],
+            'Regular .phpsh file' => ['file', '.phpsh'],
+            'Regular .phtml file' => ['file', '.phtml'],
+            'Regular .pht file' => ['file', '.pht'],
+            'Regular .phar file' => ['file', '.phar'],
+            'Regular .shtml file' => ['file', '.shtml'],
+            'Regular .cgi file' => ['file', '.cgi'],
+            'Regular .pl file' => ['file', '.pl'],
+            'Wrapped .php file ' => ['file', '.php.txt'],
+            'Wrapped .php3 file' => ['file', '.php3.txt'],
+            'Wrapped .php5 file' => ['file', '.php5.txt'],
+            'Wrapped .php7 file' => ['file', '.php7.txt'],
+            'Wrapped .phpsh file' => ['file', '.phpsh.txt'],
+            'Wrapped .phtml file' => ['file', '.phtml.txt'],
+            'Wrapped .pht file' => ['file', '.pht.txt'],
+            'Wrapped .phar file' => ['file', '.phar.txt'],
+            'Wrapped .shtml file' => ['file', '.shtml.txt'],
+            'Wrapped .cgi file' => ['file', '.cgi.txt'],
+            // allowed "Wrapped .pl file" in order to allow language specific files containing ".pl."
+            '.htaccess file' => ['', '.htaccess'],
+        ];
+
+        // Mixing with regular utf-8
+        $utf8Characters = 'Ссылка';
+        foreach ($data as $key => $value) {
+            if ($value[0] === '') {
+                continue;
+            }
+            $data[$key . ' with UTF-8 characters prepended'] = [$utf8Characters . $value[0], $value[1]];
+            $data[$key . ' with UTF-8 characters appended'] = [$value[0] . $utf8Characters, $value[1]];
+        }
+
+        // combine to single value
+        $data = array_map(
+            function (array $values): array {
+                return [implode('', $values)];
+            },
+            $data
+        );
+
+        // Encoding with UTF-16
+        foreach ($data as $key => $value) {
+            $data[$key . ' encoded with UTF-16'] = [mb_convert_encoding($value[0], 'UTF-16')];
+        }
+
+        return $data;
+    }
+
+    /**
+     * Tests whether the basic FILE_DENY_PATTERN detects denied files.
+     *
+     * @param string $deniedFile
+     * @test
+     * @dataProvider deniedFilesWithDefaultDenyPatternDataProvider
+     */
+    public function isValidDetectsNotAllowedFiles(string $deniedFile): void
+    {
+        $subject = new FileNameValidator();
+        self::assertFalse($subject->isValid($deniedFile));
+    }
+
+    /**
+     * @return array
+     */
+    public function insecureFilesDataProvider(): array
+    {
+        return [
+            'Classic php file' => ['user.php'],
+            'A random .htaccess file' => ['.htaccess'],
+            'Wrapped .php file' => ['file.php.txt'],
+        ];
+    }
+
+    /**
+     * @param string $fileName
+     * @test
+     * @dataProvider insecureFilesDataProvider
+     */
+    public function isValidAcceptsNotAllowedFilesDueToInsecureSetting(string $fileName): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] = '\\.phc$';
+        $subject = new FileNameValidator();
+        self::assertTrue($subject->isValid($fileName));
+    }
+
+    /**
+     * @return array
+     */
+    public function allowedFilesDataProvider(): array
+    {
+        return [
+            'Regular .gif file' => ['image.gif'],
+            'Regular uppercase .gif file' => ['IMAGE.gif'],
+            'UTF-8 .gif file' => ['Ссылка.gif'],
+            'Lower umlaut .jpg file' => ['üWithFile.jpg'],
+            'Upper umlaut .png file' => ['fileWithÜ.png'],
+            'Latin-1 .gif file' => ['ÉÐØ.gif'],
+            'Wrapped .pl file' => ['file.pl.txt'],
+        ];
+    }
+
+    /**
+     * Tests whether the basic file deny pattern accepts allowed files.
+     *
+     * @param string $allowedFile
+     * @test
+     * @dataProvider allowedFilesDataProvider
+     */
+    public function isValidAcceptAllowedFiles(string $allowedFile): void
+    {
+        $subject = new FileNameValidator();
+        self::assertTrue($subject->isValid($allowedFile));
+    }
+
+    /**
+     * @test
+     */
+    public function isCustomDenyPatternConfigured(): void
+    {
+        $subject = new FileNameValidator('nothing-really');
+        self::assertTrue($subject->customFileDenyPatternConfigured());
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] = 'something-else';
+        $subject = new FileNameValidator();
+        self::assertTrue($subject->customFileDenyPatternConfigured());
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] = FileNameValidator::DEFAULT_FILE_DENY_PATTERN;
+        $subject = new FileNameValidator();
+        self::assertFalse($subject->customFileDenyPatternConfigured());
+        $subject = new FileNameValidator(FileNameValidator::DEFAULT_FILE_DENY_PATTERN);
+        self::assertFalse($subject->customFileDenyPatternConfigured());
+    }
+
+    /**
+     * @test
+     */
+    public function customFileDenyPatternFindsMissingImportantParts(): void
+    {
+        $subject = new FileNameValidator('\\.php$|.php8$');
+        self::assertTrue($subject->missingImportantPatterns());
+        $subject = new FileNameValidator(FileNameValidator::DEFAULT_FILE_DENY_PATTERN);
+        self::assertFalse($subject->missingImportantPatterns());
+    }
+
+    /**
+     * Data provider for 'defaultFileDenyPatternMatchesPhpExtension' test case.
+     *
+     * @return array
+     */
+    public function phpExtensionDataProvider(): array
+    {
+        $data = [];
+        $fileName = StringUtility::getUniqueId('filename');
+        $phpExtensions = ['php', 'php3', 'php4', 'php5', 'php7', 'phpsh', 'phtml', 'pht'];
+        foreach ($phpExtensions as $extension) {
+            $data[] = [$fileName . '.' . $extension];
+            $data[] = [$fileName . '.' . $extension . '.txt'];
+        }
+        return $data;
+    }
+
+    /**
+     * Tests whether an accordant PHP extension is denied.
+     *
+     * @test
+     * @dataProvider phpExtensionDataProvider
+     * @param string $fileName
+     */
+    public function defaultFileDenyPatternMatchesPhpExtension(string $fileName): void
+    {
+        self::assertGreaterThan(0, preg_match('/' . FileNameValidator::DEFAULT_FILE_DENY_PATTERN . '/', $fileName), $fileName);
+    }
+
+    /**
+     * Tests whether an accordant PHP extension is denied.
+     *
+     * @test
+     * @dataProvider phpExtensionDataProvider
+     * @param string $fileName
+     */
+    public function invalidPhpExtensionIsDetected(string $fileName): void
+    {
+        $subject = new FileNameValidator();
+        self::assertFalse($subject->isValid($fileName));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
index fb68997d6650..73b7ea9a64dd 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
@@ -4005,137 +4005,6 @@ class GeneralUtilityTest extends UnitTestCase
         self::assertTrue(GeneralUtility::validPathStr($path));
     }
 
-    /**
-     * @return array
-     */
-    public function deniedFilesWithoutDenyPatternDataProvider(): array
-    {
-        return [
-            'Nul character in file' => ['image' . "\0" . '.gif'],
-            'Nul character in file with .php' => ['image.php' . "\0" . '.gif'],
-            'Nul character and UTF-8 in file' => ['Ссылка' . "\0" . '.gif'],
-            'Nul character and Latin-1 in file' => ['ÉÐØ' . "\0" . '.gif'],
-        ];
-    }
-
-    /**
-     * Tests whether verifyFilenameAgainstDenyPattern detects files with nul character without file deny pattern.
-     *
-     * @param string $deniedFile
-     * @test
-     * @dataProvider deniedFilesWithoutDenyPatternDataProvider
-     */
-    public function verifyNulCharacterFilesAgainstPatternWithoutFileDenyPattern(string $deniedFile)
-    {
-        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] = '';
-        self::assertFalse(GeneralUtility::verifyFilenameAgainstDenyPattern($deniedFile));
-    }
-
-    /**
-     * @return array
-     */
-    public function deniedFilesWithDefaultDenyPatternDataProvider(): array
-    {
-        $data = [
-            'Nul character in file' => ['image' . "\0", '.gif'],
-            'Nul character in file with .php' => ['image.php' . "\0", '.gif'],
-            'Nul character and UTF-8 in file' => ['Ссылка' . "\0", '.gif'],
-            'Nul character and Latin-1 in file' => ['ÉÐØ' . "\0", '.gif'],
-            'Lower umlaut .php file' => ['üWithFile', '.php'],
-            'Upper umlaut .php file' => ['fileWithÜ', '.php'],
-            'invalid UTF-8-sequence' => ["\xc0" . 'file', '.php'],
-            'Could be overlong NUL in some UTF-8 implementations, invalid in RFC3629' => ["\xc0\x80" . 'file', '.php'],
-            'Regular .php file' => ['file' , '.php'],
-            'Regular .php3 file' => ['file', '.php3'],
-            'Regular .php5 file' => ['file', '.php5'],
-            'Regular .php7 file' => ['file', '.php7'],
-            'Regular .phpsh file' => ['file', '.phpsh'],
-            'Regular .phtml file' => ['file', '.phtml'],
-            'Regular .pht file' => ['file', '.pht'],
-            'Regular .phar file' => ['file', '.phar'],
-            'Regular .shtml file' => ['file', '.shtml'],
-            'Regular .cgi file' => ['file', '.cgi'],
-            'Regular .pl file' => ['file', '.pl'],
-            'Wrapped .php file ' => ['file', '.php.txt'],
-            'Wrapped .php3 file' => ['file', '.php3.txt'],
-            'Wrapped .php5 file' => ['file', '.php5.txt'],
-            'Wrapped .php7 file' => ['file', '.php7.txt'],
-            'Wrapped .phpsh file' => ['file', '.phpsh.txt'],
-            'Wrapped .phtml file' => ['file', '.phtml.txt'],
-            'Wrapped .pht file' => ['file', '.pht.txt'],
-            'Wrapped .phar file' => ['file', '.phar.txt'],
-            'Wrapped .shtml file' => ['file', '.shtml.txt'],
-            'Wrapped .cgi file' => ['file', '.cgi.txt'],
-            // allowed "Wrapped .pl file" in order to allow language specific files containing ".pl."
-            '.htaccess file' => ['', '.htaccess'],
-        ];
-
-        // Mixing with regular utf-8
-        $utf8Characters = 'Ссылка';
-        foreach ($data as $key => $value) {
-            if ($value[0] === '') {
-                continue;
-            }
-            $data[$key . ' with UTF-8 characters prepended'] = [$utf8Characters . $value[0], $value[1]];
-            $data[$key . ' with UTF-8 characters appended'] = [$value[0] . $utf8Characters, $value[1]];
-        }
-
-        // combine to single value
-        $data = array_map(
-            function (array $values): array {
-                return [implode('', $values)];
-            },
-            $data
-        );
-
-        // Encoding with UTF-16
-        foreach ($data as $key => $value) {
-            $data[$key . ' encoded with UTF-16'] = [mb_convert_encoding($value[0], 'UTF-16')];
-        }
-
-        return $data;
-    }
-
-    /**
-     * Tests whether verifyFilenameAgainstDenyPattern detects denied files.
-     *
-     * @param string $deniedFile
-     * @test
-     * @dataProvider deniedFilesWithDefaultDenyPatternDataProvider
-     */
-    public function verifyFilenameAgainstDenyPatternDetectsNotAllowedFiles($deniedFile)
-    {
-        self::assertFalse(GeneralUtility::verifyFilenameAgainstDenyPattern($deniedFile));
-    }
-
-    /**
-     * @return array
-     */
-    public function allowedFilesDataProvider(): array
-    {
-        return [
-            'Regular .gif file' => ['image.gif'],
-            'Regular uppercase .gif file' => ['IMAGE.gif'],
-            'UTF-8 .gif file' => ['Ссылка.gif'],
-            'Lower umlaut .jpg file' => ['üWithFile.jpg'],
-            'Upper umlaut .png file' => ['fileWithÜ.png'],
-            'Latin-1 .gif file' => ['ÉÐØ.gif'],
-            'Wrapped .pl file' => ['file.pl.txt'],
-        ];
-    }
-
-    /**
-     * Tests whether verifyFilenameAgainstDenyPattern accepts allowed files.
-     *
-     * @param string $allowedFile
-     * @test
-     * @dataProvider allowedFilesDataProvider
-     */
-    public function verifyFilenameAgainstDenyPatternAcceptAllowedFiles(string $allowedFile)
-    {
-        self::assertTrue(GeneralUtility::verifyFilenameAgainstDenyPattern($allowedFile));
-    }
-
     /////////////////////////////////////////////////////////////////////////////////////
     // Tests concerning copyDirectory
     /////////////////////////////////////////////////////////////////////////////////////
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php
index cf89a3c5138c..9477411b52c0 100644
--- a/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php
@@ -86,4 +86,135 @@ class GeneralUtilityTest extends UnitTestCase
             ]
         ];
     }
+
+    /**
+     * @return array
+     */
+    public function deniedFilesWithoutDenyPatternDataProvider(): array
+    {
+        return [
+            'Nul character in file' => ['image' . "\0" . '.gif'],
+            'Nul character in file with .php' => ['image.php' . "\0" . '.gif'],
+            'Nul character and UTF-8 in file' => ['Ссылка' . "\0" . '.gif'],
+            'Nul character and Latin-1 in file' => ['ÉÐØ' . "\0" . '.gif'],
+        ];
+    }
+
+    /**
+     * Tests whether verifyFilenameAgainstDenyPattern detects files with nul character without file deny pattern.
+     *
+     * @param string $deniedFile
+     * @test
+     * @dataProvider deniedFilesWithoutDenyPatternDataProvider
+     */
+    public function verifyNulCharacterFilesAgainstPatternWithoutFileDenyPattern(string $deniedFile)
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] = '';
+        self::assertFalse(GeneralUtility::verifyFilenameAgainstDenyPattern($deniedFile));
+    }
+
+    /**
+     * @return array
+     */
+    public function deniedFilesWithDefaultDenyPatternDataProvider(): array
+    {
+        $data = [
+            'Nul character in file' => ['image' . "\0", '.gif'],
+            'Nul character in file with .php' => ['image.php' . "\0", '.gif'],
+            'Nul character and UTF-8 in file' => ['Ссылка' . "\0", '.gif'],
+            'Nul character and Latin-1 in file' => ['ÉÐØ' . "\0", '.gif'],
+            'Lower umlaut .php file' => ['üWithFile', '.php'],
+            'Upper umlaut .php file' => ['fileWithÜ', '.php'],
+            'invalid UTF-8-sequence' => ["\xc0" . 'file', '.php'],
+            'Could be overlong NUL in some UTF-8 implementations, invalid in RFC3629' => ["\xc0\x80" . 'file', '.php'],
+            'Regular .php file' => ['file' , '.php'],
+            'Regular .php3 file' => ['file', '.php3'],
+            'Regular .php5 file' => ['file', '.php5'],
+            'Regular .php7 file' => ['file', '.php7'],
+            'Regular .phpsh file' => ['file', '.phpsh'],
+            'Regular .phtml file' => ['file', '.phtml'],
+            'Regular .pht file' => ['file', '.pht'],
+            'Regular .phar file' => ['file', '.phar'],
+            'Regular .shtml file' => ['file', '.shtml'],
+            'Regular .cgi file' => ['file', '.cgi'],
+            'Regular .pl file' => ['file', '.pl'],
+            'Wrapped .php file ' => ['file', '.php.txt'],
+            'Wrapped .php3 file' => ['file', '.php3.txt'],
+            'Wrapped .php5 file' => ['file', '.php5.txt'],
+            'Wrapped .php7 file' => ['file', '.php7.txt'],
+            'Wrapped .phpsh file' => ['file', '.phpsh.txt'],
+            'Wrapped .phtml file' => ['file', '.phtml.txt'],
+            'Wrapped .pht file' => ['file', '.pht.txt'],
+            'Wrapped .phar file' => ['file', '.phar.txt'],
+            'Wrapped .shtml file' => ['file', '.shtml.txt'],
+            'Wrapped .cgi file' => ['file', '.cgi.txt'],
+            // allowed "Wrapped .pl file" in order to allow language specific files containing ".pl."
+            '.htaccess file' => ['', '.htaccess'],
+        ];
+
+        // Mixing with regular utf-8
+        $utf8Characters = 'Ссылка';
+        foreach ($data as $key => $value) {
+            if ($value[0] === '') {
+                continue;
+            }
+            $data[$key . ' with UTF-8 characters prepended'] = [$utf8Characters . $value[0], $value[1]];
+            $data[$key . ' with UTF-8 characters appended'] = [$value[0] . $utf8Characters, $value[1]];
+        }
+
+        // combine to single value
+        $data = array_map(
+            function (array $values): array {
+                return [implode('', $values)];
+            },
+            $data
+        );
+
+        // Encoding with UTF-16
+        foreach ($data as $key => $value) {
+            $data[$key . ' encoded with UTF-16'] = [mb_convert_encoding($value[0], 'UTF-16')];
+        }
+
+        return $data;
+    }
+
+    /**
+     * Tests whether verifyFilenameAgainstDenyPattern detects denied files.
+     *
+     * @param string $deniedFile
+     * @test
+     * @dataProvider deniedFilesWithDefaultDenyPatternDataProvider
+     */
+    public function verifyFilenameAgainstDenyPatternDetectsNotAllowedFiles($deniedFile)
+    {
+        self::assertFalse(GeneralUtility::verifyFilenameAgainstDenyPattern($deniedFile));
+    }
+
+    /**
+     * @return array
+     */
+    public function allowedFilesDataProvider(): array
+    {
+        return [
+            'Regular .gif file' => ['image.gif'],
+            'Regular uppercase .gif file' => ['IMAGE.gif'],
+            'UTF-8 .gif file' => ['Ссылка.gif'],
+            'Lower umlaut .jpg file' => ['üWithFile.jpg'],
+            'Upper umlaut .png file' => ['fileWithÜ.png'],
+            'Latin-1 .gif file' => ['ÉÐØ.gif'],
+            'Wrapped .pl file' => ['file.pl.txt'],
+        ];
+    }
+
+    /**
+     * Tests whether verifyFilenameAgainstDenyPattern accepts allowed files.
+     *
+     * @param string $allowedFile
+     * @test
+     * @dataProvider allowedFilesDataProvider
+     */
+    public function verifyFilenameAgainstDenyPatternAcceptAllowedFiles(string $allowedFile)
+    {
+        self::assertTrue(GeneralUtility::verifyFilenameAgainstDenyPattern($allowedFile));
+    }
 }
diff --git a/typo3/sysext/filelist/Classes/Controller/File/CreateFolderController.php b/typo3/sysext/filelist/Classes/Controller/File/CreateFolderController.php
index 5f38159838e2..8f8aca8a7283 100644
--- a/typo3/sysext/filelist/Classes/Controller/File/CreateFolderController.php
+++ b/typo3/sysext/filelist/Classes/Controller/File/CreateFolderController.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
@@ -208,8 +209,9 @@ class CreateFolderController
             // Create a list of allowed file extensions with the readable format "youtube, vimeo" etc.
             $fileExtList = [];
             $onlineMediaFileExt = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
+            $fileNameVerifier = GeneralUtility::makeInstance(FileNameValidator::class);
             foreach ($onlineMediaFileExt as $fileExt) {
-                if (GeneralUtility::verifyFilenameAgainstDenyPattern('.' . $fileExt)) {
+                if ($fileNameVerifier->isValid('.' . $fileExt)) {
                     $fileExtList[] = strtoupper(htmlspecialchars($fileExt));
                 }
             }
@@ -221,7 +223,7 @@ class CreateFolderController
             $fileExtList = [];
             $textFileExt = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], true);
             foreach ($textFileExt as $fileExt) {
-                if (GeneralUtility::verifyFilenameAgainstDenyPattern('.' . $fileExt)) {
+                if ($fileNameVerifier->isValid('.' . $fileExt)) {
                     $fileExtList[] = strtoupper(htmlspecialchars($fileExt));
                 }
             }
diff --git a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php
index 5c3693bb9353..34b9427c83c4 100644
--- a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php
+++ b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php
@@ -18,6 +18,7 @@ namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
 use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Resource\File as File;
 use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Domain\Model\AbstractFileFolder;
 use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference;
@@ -193,7 +194,7 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter
         array $uploadInfo,
         PropertyMappingConfigurationInterface $configuration
     ): ExtbaseFileReference {
-        if (!GeneralUtility::verifyFilenameAgainstDenyPattern($uploadInfo['name'])) {
+        if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($uploadInfo['name'])) {
             throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1471710357);
         }
 
diff --git a/typo3/sysext/impexp/Classes/Import.php b/typo3/sysext/impexp/Classes/Import.php
index 29ad36171db1..9ab4e98a04b5 100644
--- a/typo3/sysext/impexp/Classes/Import.php
+++ b/typo3/sysext/impexp/Classes/Import.php
@@ -25,6 +25,7 @@ use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\FileInterface;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Resource\StorageRepository;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -1544,7 +1545,7 @@ class Import extends ImportExport
             }
         }
         $fI = GeneralUtility::split_fileref($fileName);
-        if (!GeneralUtility::verifyFilenameAgainstDenyPattern($fI['file'])) {
+        if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($fI['file'])) {
             $this->error('ERROR: Filename "' . $fileName . '" failed against extension check or deny-pattern!');
             return false;
         }
diff --git a/typo3/sysext/impexp/Classes/ImportExport.php b/typo3/sysext/impexp/Classes/ImportExport.php
index 0f1c6bfcd688..f80e6e703fb0 100644
--- a/typo3/sysext/impexp/Classes/ImportExport.php
+++ b/typo3/sysext/impexp/Classes/ImportExport.php
@@ -22,6 +22,7 @@ use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
 use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
 use TYPO3\CMS\Core\Utility\DebugUtility;
 use TYPO3\CMS\Core\Utility\DiffUtility;
@@ -764,7 +765,7 @@ abstract class ImportExport
                 $fileProcObj = $this->getFileProcObj();
                 if ($fileProcObj->actionPerms['addFile']) {
                     $testFI = GeneralUtility::split_fileref(Environment::getPublicPath() . '/' . $fI['relFileName']);
-                    if (!GeneralUtility::verifyFilenameAgainstDenyPattern($testFI['file'])) {
+                    if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($testFI['file'])) {
                         $pInfo['msg'] .= 'File extension was not allowed!';
                     }
                 } else {
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ConstantMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ConstantMatcher.php
index cc0f0060167b..b745c1cb20b6 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ConstantMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ConstantMatcher.php
@@ -220,4 +220,14 @@ return [
             'Deprecation-89866-Global-TYPO3-information-related-constants.rst'
         ]
     ],
+    'FILE_DENY_PATTERN_DEFAULT' => [
+        'restFiles' => [
+            'Deprecation-90147-UnifiedFileNameValidator.rst'
+        ]
+    ],
+    'PHP_EXTENSIONS_DEFAULT' => [
+        'restFiles' => [
+            'Deprecation-90147-UnifiedFileNameValidator.rst'
+        ]
+    ],
 ];
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
index d5991e6a721c..e0384608f01e 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
@@ -973,4 +973,11 @@ return [
             'Deprecation-90800-GeneralUtilityisRunningOnCgiServerApi.rst',
         ],
     ],
+    'TYPO3\CMS\Core\Utility\GeneralUtility::verifyFilenameAgainstDenyPattern' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-90147-UnifiedFileNameValidator.rst'
+        ],
+    ],
 ];
diff --git a/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php b/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
index 354d02ba380b..2ec45c6185f7 100644
--- a/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
+++ b/typo3/sysext/recordlist/Classes/View/FolderUtilityRenderer.php
@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Resource\Folder;
 use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Recordlist\Tree\View\LinkParameterProviderInterface;
@@ -124,8 +125,9 @@ class FolderUtilityRenderer
         $lang = $this->getLanguageService();
         // Create a list of allowed file extensions with the readable format "youtube, vimeo" etc.
         $fileExtList = [];
+        $fileNameVerifier = GeneralUtility::makeInstance(FileNameValidator::class);
         foreach ($allowedExtensions as $fileExt) {
-            if (GeneralUtility::verifyFilenameAgainstDenyPattern('.' . $fileExt)) {
+            if ($fileNameVerifier->isValid('.' . $fileExt)) {
                 $fileExtList[] = '<span class="label label-success">'
                     . strtoupper(htmlspecialchars($fileExt)) . '</span>';
             }
@@ -185,7 +187,7 @@ class FolderUtilityRenderer
         $fileExtList = [];
         $onlineMediaFileExt = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
         foreach ($onlineMediaFileExt as $fileExt) {
-            if (GeneralUtility::verifyFilenameAgainstDenyPattern('.' . $fileExt)
+            if ($fileNameVerifier->isValid('.' . $fileExt)
                 && (empty($allowedExtensions) || in_array($fileExt, $allowedExtensions, true))
             ) {
                 $fileExtList[] = '<span class="label label-success">'
diff --git a/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php b/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
index d435a1d12ebb..b532d19d7f7a 100644
--- a/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
+++ b/typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Reports\RequestAwareStatusProviderInterface;
 use TYPO3\CMS\Reports\Status as ReportStatus;
@@ -202,16 +203,14 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
         $value = $this->getLanguageService()->getLL('status_ok');
         $message = '';
         $severity = ReportStatus::OK;
-        $defaultParts = GeneralUtility::trimExplode('|', FILE_DENY_PATTERN_DEFAULT, true);
-        $givenParts = GeneralUtility::trimExplode('|', $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'], true);
-        $result = array_intersect($defaultParts, $givenParts);
 
-        if ($defaultParts !== $result) {
+        $fileAccessCheck = GeneralUtility::makeInstance(FileNameValidator::class);
+        if ($fileAccessCheck->missingImportantPatterns()) {
             $value = $this->getLanguageService()->getLL('status_insecure');
             $severity = ReportStatus::ERROR;
             $message = sprintf(
                 $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_deny_pattern_partsNotPresent'),
-                '<br /><pre>' . htmlspecialchars(FILE_DENY_PATTERN_DEFAULT) . '</pre><br />'
+                '<br /><pre>' . htmlspecialchars($fileAccessCheck::DEFAULT_FILE_DENY_PATTERN) . '</pre><br />'
             );
         }
 
@@ -230,8 +229,9 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
         $message = '';
         $severity = ReportStatus::OK;
 
-        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] != FILE_DENY_PATTERN_DEFAULT
-            && GeneralUtility::verifyFilenameAgainstDenyPattern('.htaccess')) {
+        $fileNameAccess = GeneralUtility::makeInstance(FileNameValidator::class);
+        if ($fileNameAccess->customFileDenyPatternConfigured()
+            && $fileNameAccess->isValid('.htaccess')) {
             $value = $this->getLanguageService()->getLL('status_insecure');
             $severity = ReportStatus::ERROR;
             $message = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_deny_htaccess');
-- 
GitLab