diff --git a/core/includes/file.inc b/core/includes/file.inc
index c40851dedb906cb1c797fefcb96feec30f9e91f0..33fd9f8b817036737c887be8dcfc0784871473e9 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -159,8 +159,8 @@ function file_build_uri($path) {
  * exploit.php_.pps.
  *
  * Specifically, this function adds an underscore to all extensions that are
- * between 2 and 5 characters in length, internal to the file name, and not
- * included in $extensions.
+ * between 2 and 5 characters in length, internal to the file name, and either
+ * included in the list of unsafe extensions, or not included in $extensions.
  *
  * Function behavior is also controlled by the configuration
  * 'system.file:allow_insecure_uploads'. If it evaluates to TRUE, no alterations
@@ -168,7 +168,8 @@ function file_build_uri($path) {
  * @param $filename
  *   File name to modify.
  * @param $extensions
- *   A space-separated list of extensions that should not be altered.
+ *   A space-separated list of extensions that should not be altered. Note that
+ *   extensions that are unsafe will be altered regardless of this parameter.
  * @param $alerts
  *   If TRUE, \Drupal::messenger()->addStatus() will be called to display
  *   a message if the file name was changed.
@@ -187,6 +188,12 @@ function file_munge_filename($filename, $extensions, $alerts = TRUE) {
 
     $allowed_extensions = array_unique(explode(' ', strtolower(trim($extensions))));
 
+    // Remove unsafe extensions from the allowed list of extensions.
+    // @todo https://www.drupal.org/project/drupal/issues/3032390 Make the list
+    //   of unsafe extensions a constant. The list is copied from
+    //   FILE_INSECURE_EXTENSION_REGEX.
+    $allowed_extensions = array_diff($allowed_extensions, explode('|', 'phar|php|pl|py|cgi|asp|js'));
+
     // Split the filename up by periods. The first part becomes the basename
     // the last part the final extension.
     $filename_parts = explode('.', $filename);
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 596a860ebeda2bdae5da8b4186f78165e909d0b8..0e5c0b31868d37e3dc8ee02e8f2a6bd9b5d5755b 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -281,7 +281,17 @@ function file_validate(FileInterface $file, $validators = []) {
   }
 
   // Let other modules perform validation on the new file.
-  return array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
+  $errors = array_merge($errors, \Drupal::moduleHandler()->invokeAll('file_validate', [$file]));
+
+  // Ensure the file does not contain a malicious extension. At this point
+  // _file_save_upload_single() will have munged the file so it does not contain
+  // a malicious extension. Contributed and custom code that calls this method
+  // needs to take similar steps if they need to permit files with malicious
+  // extensions to be uploaded.
+  if (empty($errors) && !\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())) {
+    $errors[] = t('For security reasons, your upload has been rejected.');
+  }
+  return $errors;
 }
 
 /**
@@ -978,25 +988,36 @@ function _file_save_upload_single(\SplFileInfo $file_info, $form_field_name, $va
     $validators['file_validate_extensions'][0] = $extensions;
   }
 
-  if (!empty($extensions)) {
-    // Munge the filename to protect against possible malicious extension
-    // hiding within an unknown file type (ie: filename.html.foo).
-    $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
-  }
-
-  // Rename potentially executable files, to help prevent exploits (i.e. will
-  // rename filename.php.foo and filename.php to filename.php.foo.txt and
-  // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
-  // evaluates to TRUE.
-  if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
-    $file->setMimeType('text/plain');
-    // The destination filename will also later be used to create the URI.
-    $file->setFilename($file->getFilename() . '.txt');
-    // The .txt extension may not be in the allowed list of extensions. We have
-    // to add it here or else the file upload will fail.
+  //  Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
+  if (!\Drupal::config('system.file')->get('allow_insecure_uploads')) {
     if (!empty($extensions)) {
-      $validators['file_validate_extensions'][0] .= ' txt';
-      \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
+      // Munge the filename to protect against possible malicious extension
+      // hiding within an unknown file type (ie: filename.html.foo).
+      $file->setFilename(file_munge_filename($file->getFilename(), $extensions));
+    }
+
+    // Rename potentially executable files, to help prevent exploits (i.e. will
+    // rename filename.php.foo and filename.php to filename.php_.foo_.txt and
+    // filename.php_.txt, respectively).
+    if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $file->getFilename())) {
+      // If the file will be rejected anyway due to a disallowed extension, it
+      // should not be renamed; rather, we'll let file_validate_extensions()
+      // reject it below.
+      if (!isset($validators['file_validate_extensions']) || empty(file_validate_extensions($file, $extensions))) {
+        $file->setMimeType('text/plain');
+        $filename = $file->getFilename();
+        if (substr($filename, -4) != '.txt') {
+          // The destination filename will also later be used to create the URI.
+          $filename .= '.txt';
+        }
+        $file->setFilename(file_munge_filename($filename, $extensions));
+        \Drupal::messenger()->addStatus(t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
+        // The .txt extension may not be in the allowed list of extensions. We
+        // have to add it here or else the file upload will fail.
+        if (!empty($validators['file_validate_extensions'][0])) {
+          $validators['file_validate_extensions'][0] .= ' txt';
+        }
+      }
     }
   }
 
diff --git a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
index 1aaa5a753bad49b4e23c167331b839b7756f063e..3c7180a0b03bb7a48fefa094959d9967b86bd443 100644
--- a/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -468,26 +468,42 @@ protected function validate(FileInterface $file, array $validators) {
    *   The prepared/munged filename.
    */
   protected function prepareFilename($filename, array &$validators) {
-    if (!empty($validators['file_validate_extensions'][0])) {
-      // If there is a file_validate_extensions validator and a list of
-      // valid extensions, munge the filename to protect against possible
-      // malicious extension hiding within an unknown file type. For example,
-      // "filename.html.foo".
-      $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
-    }
-
-    // Rename potentially executable files, to help prevent exploits (i.e. will
-    // rename filename.php.foo and filename.php to filename.php.foo.txt and
-    // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
-    // evaluates to TRUE.
-    if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
-      // The destination filename will also later be used to create the URI.
-      $filename .= '.txt';
-
-      // The .txt extension may not be in the allowed list of extensions. We
-      // have to add it here or else the file upload will fail.
+    // Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
+    if (!$this->systemFileConfig->get('allow_insecure_uploads')) {
       if (!empty($validators['file_validate_extensions'][0])) {
-        $validators['file_validate_extensions'][0] .= ' txt';
+        // If there is a file_validate_extensions validator and a list of
+        // valid extensions, munge the filename to protect against possible
+        // malicious extension hiding within an unknown file type. For example,
+        // "filename.html.foo".
+        $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
+      }
+
+      // Rename potentially executable files, to help prevent exploits (i.e.
+      // will rename filename.php.foo and filename.php to filename._php._foo.txt
+      // and filename._php.txt, respectively).
+      if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename)) {
+        // If the file will be rejected anyway due to a disallowed extension, it
+        // should not be renamed; rather, we'll let file_validate_extensions()
+        // reject it below.
+        $passes_validation = FALSE;
+        if (!empty($validators['file_validate_extensions'][0])) {
+          $file = File::create([]);
+          $file->setFilename($filename);
+          $passes_validation = empty(file_validate_extensions($file, $validators['file_validate_extensions'][0]));
+        }
+        if (empty($validators['file_validate_extensions'][0]) || $passes_validation) {
+          if ((substr($filename, -4) != '.txt')) {
+            // The destination filename will also later be used to create the URI.
+            $filename .= '.txt';
+          }
+          $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0] ?? '');
+
+          // The .txt extension may not be in the allowed list of extensions. We
+          // have to add it here or else the file upload will fail.
+          if (!empty($validators['file_validate_extensions'][0])) {
+            $validators['file_validate_extensions'][0] .= ' txt';
+          }
+        }
       }
     }
 
diff --git a/core/modules/file/tests/file_test/src/Form/FileTestForm.php b/core/modules/file/tests/file_test/src/Form/FileTestForm.php
index 08c4a57ffd7a3a0c03efc103aa9bcddd7ab92b71..c9fc1f0e93111e593b87988463e3563c04ad77f4 100644
--- a/core/modules/file/tests/file_test/src/Form/FileTestForm.php
+++ b/core/modules/file/tests/file_test/src/Form/FileTestForm.php
@@ -49,9 +49,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     $form['allow_all_extensions'] = [
-      '#type' => 'checkbox',
       '#title' => t('Allow all extensions?'),
-      '#default_value' => FALSE,
+      '#type' => 'radios',
+      '#options' => [
+        'false' => 'No',
+        'empty_array' => 'Empty array',
+        'empty_string' => 'Empty string',
+      ],
+      '#default_value' => 'false',
     ];
 
     $form['is_image_file'] = [
@@ -92,9 +97,13 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
       $validators['file_validate_is_image'] = [];
     }
 
-    if ($form_state->getValue('allow_all_extensions')) {
+    $allow = $form_state->getValue('allow_all_extensions');
+    if ($allow === 'empty_array') {
       $validators['file_validate_extensions'] = [];
     }
+    elseif ($allow === 'empty_string') {
+      $validators['file_validate_extensions'] = [''];
+    }
     elseif (!$form_state->isValueEmpty('extensions')) {
       $validators['file_validate_extensions'] = [$form_state->getValue('extensions')];
     }
diff --git a/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php b/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php
index 10127dc94cfefa105d067ef28d1b4be42afe7c47..79bff3ad7b5c8a88052464049ba87bcbcecde455 100644
--- a/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php
+++ b/core/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php
@@ -90,9 +90,14 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
 
     $form['allow_all_extensions'] = [
-      '#type' => 'checkbox',
-      '#title' => $this->t('Allow all extensions?'),
-      '#default_value' => FALSE,
+      '#title' => t('Allow all extensions?'),
+      '#type' => 'radios',
+      '#options' => [
+        'false' => 'No',
+        'empty_array' => 'Empty array',
+        'empty_string' => 'Empty string',
+      ],
+      '#default_value' => 'false',
     ];
 
     $form['is_image_file'] = [
@@ -139,9 +144,13 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
       $validators['file_validate_is_image'] = [];
     }
 
-    if ($form_state->getValue('allow_all_extensions')) {
+    $allow = $form_state->getValue('allow_all_extensions');
+    if ($allow === 'empty_array') {
       $validators['file_validate_extensions'] = [];
     }
+    elseif ($allow === 'empty_string') {
+      $validators['file_validate_extensions'] = [''];
+    }
     elseif (!$form_state->isValueEmpty('extensions')) {
       $validators['file_validate_extensions'] = [$form_state->getValue('extensions')];
     }
diff --git a/core/modules/file/tests/src/Functional/SaveUploadFormTest.php b/core/modules/file/tests/src/Functional/SaveUploadFormTest.php
index 643dbcf37c62833c7a7562597238d504d2c3e012..c80ddf2420892c930c5a34337cf09e74021024f1 100644
--- a/core/modules/file/tests/src/Functional/SaveUploadFormTest.php
+++ b/core/modules/file/tests/src/Functional/SaveUploadFormTest.php
@@ -198,7 +198,7 @@ public function testHandleExtension() {
     $edit = [
       'file_test_replace' => FileSystemInterface::EXISTS_REPLACE,
       'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
-      'allow_all_extensions' => TRUE,
+      'allow_all_extensions' => 'empty_array',
     ];
     $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
     $this->assertSession()->statusCodeEquals(200);
@@ -227,7 +227,7 @@ public function testHandleDangerousFile() {
 
     $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
     $this->assertSession()->statusCodeEquals(200);
-    $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
+    $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '_.txt' . '</em>';
     $this->assertRaw($message);
     $this->assertRaw(t('File MIME type is text/plain.'));
     $this->assertRaw(t('You WIN!'));
@@ -262,7 +262,8 @@ public function testHandleFileMunge() {
     $file_system = \Drupal::service('file_system');
     // Ensure insecure uploads are disabled for this test.
     $this->config('system.file')->set('allow_insecure_uploads', 0)->save();
-    $this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);
+    $original_uri = $this->image->getFileUri();
+    $this->image = file_move($this->image, $original_uri . '.foo.' . $this->imageExtension);
 
     // Reset the hook counters to get rid of the 'move' we just called.
     file_test_reset();
@@ -286,13 +287,37 @@ public function testHandleFileMunge() {
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(['validate', 'insert']);
 
+    // Test with uppercase extensions.
+    $this->image = file_move($this->image, $original_uri . '.foo2.' . $this->imageExtension);
+    // Reset the hook counters.
+    file_test_reset();
+    $extensions = $this->imageExtension;
+    $edit = [
+      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
+      'extensions' => mb_strtoupper($extensions),
+    ];
+
+    $munged_filename = $this->image->getFilename();
+    $munged_filename = substr($munged_filename, 0, strrpos($munged_filename, '.'));
+    $munged_filename .= '_.' . $this->imageExtension;
+
+    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('File name is @filename', ['@filename' => $munged_filename]));
+    $this->assertRaw(t('You WIN!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate', 'insert']);
+
     // Ensure we don't munge files if we're allowing any extension.
     // Reset the hook counters.
     file_test_reset();
 
+    // Ensure we don't munge files if we're allowing any extension.
     $edit = [
       'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
-      'allow_all_extensions' => TRUE,
+      'allow_all_extensions' => 'empty_array',
     ];
 
     $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
@@ -303,6 +328,24 @@ public function testHandleFileMunge() {
 
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(['validate', 'insert']);
+
+    // Ensure that setting $validators['file_validate_extensions'] = ['']
+    // rejects all files.
+    // Reset the hook counters.
+    file_test_reset();
+
+    $edit = [
+      'files[file_test_upload][]' => $file_system->realpath($this->image->getFileUri()),
+      'allow_all_extensions' => 'empty_string',
+    ];
+
+    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertNoRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('Epic upload FAIL!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate']);
   }
 
   /**
@@ -467,7 +510,6 @@ public function testCombinedErrorMessages() {
     $submit_xpath = $this->assertSession()->buttonExists('Submit')->getXpath();
     $form = $client->getCrawler()->filterXPath($submit_xpath)->form();
     $edit = [
-      'allow_all_extensions' => FALSE,
       'is_image_file' => TRUE,
       'extensions' => 'jpeg',
     ];
diff --git a/core/modules/file/tests/src/Functional/SaveUploadTest.php b/core/modules/file/tests/src/Functional/SaveUploadTest.php
index 2e765cbc72a01d81a3035ba072293f7d08e9cfa8..3b185fc10246ccabfc14cea0bacd5632bfa4f339 100644
--- a/core/modules/file/tests/src/Functional/SaveUploadTest.php
+++ b/core/modules/file/tests/src/Functional/SaveUploadTest.php
@@ -219,7 +219,7 @@ public function testHandleExtension() {
     $edit = [
       'file_test_replace' => FileSystemInterface::EXISTS_REPLACE,
       'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
-      'allow_all_extensions' => TRUE,
+      'allow_all_extensions' => 'empty_array',
     ];
     $this->drupalPostForm('file-test/upload', $edit, 'Submit');
     $this->assertSession()->statusCodeEquals(200);
@@ -228,6 +228,27 @@ public function testHandleExtension() {
 
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(['validate', 'load', 'update']);
+
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Now tell file_save_upload() to allow any extension and try and upload a
+    // malicious file.
+    $edit = [
+      'file_test_replace' => FileSystemInterface::EXISTS_REPLACE,
+      'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->phpfile->uri),
+      'allow_all_extensions' => 'empty_array',
+      'is_image_file' => FALSE,
+    ];
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '_.txt' . '</em>';
+    $this->assertRaw($message);
+    $this->assertSession()->pageTextContains('File name is php-2.php_.txt.');
+    $this->assertRaw(t('File MIME type is text/plain.'));
+    $this->assertRaw(t('You WIN!'));
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate', 'insert']);
   }
 
   /**
@@ -235,8 +256,8 @@ public function testHandleExtension() {
    */
   public function testHandleDangerousFile() {
     $config = $this->config('system.file');
-    // Allow the .php extension and make sure it gets renamed to .txt for
-    // safety. Also check to make sure its MIME type was changed.
+    // Allow the .php extension and make sure it gets munged and given a .txt
+    // extension for safety. Also check to make sure its MIME type was changed.
     $edit = [
       'file_test_replace' => FileSystemInterface::EXISTS_REPLACE,
       'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->phpfile->uri),
@@ -246,9 +267,9 @@ public function testHandleDangerousFile() {
 
     $this->drupalPostForm('file-test/upload', $edit, 'Submit');
     $this->assertSession()->statusCodeEquals(200);
-    $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '.txt' . '</em>';
+    $message = t('For security reasons, your upload has been renamed to') . ' <em class="placeholder">' . $this->phpfile->filename . '_.txt' . '</em>';
     $this->assertRaw($message);
-    $this->assertSession()->pageTextContains('File name is php-2.php.txt.');
+    $this->assertSession()->pageTextContains('File name is php-2.php_.txt.');
     $this->assertRaw(t('File MIME type is text/plain.'));
     $this->assertRaw(t('You WIN!'));
 
@@ -270,8 +291,39 @@ public function testHandleDangerousFile() {
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(['validate', 'insert']);
 
-    // Turn off insecure uploads.
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Even with insecure uploads allowed, the .php file should not be uploaded
+    // if it is not explicitly included in the list of allowed extensions.
+    $edit['extensions'] = 'foo';
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $edit['extensions'] . '</em>';
+    $this->assertRaw($message);
+    $this->assertRaw(t('Epic upload FAIL!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate']);
+
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Turn off insecure uploads, then try the same thing as above (ensure that
+    // the .php file is still rejected since it's not in the list of allowed
+    // extensions).
     $config->set('allow_insecure_uploads', 0)->save();
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $message = t('Only files with the following extensions are allowed:') . ' <em class="placeholder">' . $edit['extensions'] . '</em>';
+    $this->assertRaw($message);
+    $this->assertRaw(t('Epic upload FAIL!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate']);
+
+    // Reset the hook counters.
+    file_test_reset();
   }
 
   /**
@@ -280,7 +332,8 @@ public function testHandleDangerousFile() {
   public function testHandleFileMunge() {
     // Ensure insecure uploads are disabled for this test.
     $this->config('system.file')->set('allow_insecure_uploads', 0)->save();
-    $this->image = file_move($this->image, $this->image->getFileUri() . '.foo.' . $this->imageExtension);
+    $original_image_uri = $this->image->getFileUri();
+    $this->image = file_move($this->image, $original_image_uri . '.foo.' . $this->imageExtension);
 
     // Reset the hook counters to get rid of the 'move' we just called.
     file_test_reset();
@@ -304,13 +357,34 @@ public function testHandleFileMunge() {
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(['validate', 'insert']);
 
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Ensure we don't munge the .foo extension if it is in the list of allowed
+    // extensions.
+    $extensions = 'foo ' . $this->imageExtension;
+    $edit = [
+        'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
+        'extensions' => $extensions,
+      ];
+
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertNoRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('File name is @filename', ['@filename' => $this->image->getFilename()]));
+    $this->assertRaw(t('You WIN!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate', 'insert']);
+
     // Ensure we don't munge files if we're allowing any extension.
+    $this->image = file_move($this->image, $original_image_uri . '.foo.txt.' . $this->imageExtension);
     // Reset the hook counters.
     file_test_reset();
 
     $edit = [
       'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
-      'allow_all_extensions' => TRUE,
+      'allow_all_extensions' => 'empty_array',
     ];
 
     $this->drupalPostForm('file-test/upload', $edit, 'Submit');
@@ -321,6 +395,83 @@ public function testHandleFileMunge() {
 
     // Check that the correct hooks were called.
     $this->assertFileHooksCalled(['validate', 'insert']);
+
+    // Test that a dangerous extension such as .php is munged even if it is in
+    // the list of allowed extensions.
+    $this->image = file_move($this->image, $original_image_uri . '.php.' . $this->imageExtension);
+    // Reset the hook counters.
+    file_test_reset();
+
+    $extensions = 'php ' . $this->imageExtension;
+    $edit = [
+        'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
+        'extensions' => $extensions,
+      ];
+
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('File name is @filename', ['@filename' => 'image-test.png.php_.png']));
+    $this->assertRaw(t('You WIN!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate', 'insert']);
+
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Dangerous extensions are munged even when all extensions are allowed.
+    $edit = [
+      'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
+      'allow_all_extensions' => 'empty_array',
+    ];
+
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('File name is @filename.', ['@filename' => 'image-test.png_.php_.png_.txt']));
+    $this->assertRaw(t('You WIN!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate', 'insert']);
+
+    // Dangerous extensions are munged if is renamed to end in .txt.
+    $this->image = file_move($this->image, $original_image_uri . '.cgi.' . $this->imageExtension . '.txt');
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Dangerous extensions are munged even when all extensions are allowed.
+    $edit = [
+      'files[file_test_upload]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
+      'allow_all_extensions' => 'empty_array',
+    ];
+
+    $this->drupalPostForm('file-test/upload', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('File name is @filename.', ['@filename' => 'image-test.png_.cgi_.png_.txt']));
+    $this->assertRaw(t('You WIN!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate', 'insert']);
+
+    // Reset the hook counters.
+    file_test_reset();
+
+    // Ensure that setting $validators['file_validate_extensions'] = ['']
+    // rejects all files without munging or renaming.
+    $edit = [
+      'files[file_test_upload][]' => \Drupal::service('file_system')->realpath($this->image->getFileUri()),
+      'allow_all_extensions' => 'empty_string',
+    ];
+
+    $this->drupalPostForm('file-test/save_upload_from_form_test', $edit, 'Submit');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertNoRaw(t('For security reasons, your upload has been renamed'));
+    $this->assertRaw(t('Epic upload FAIL!'));
+
+    // Check that the correct hooks were called.
+    $this->assertFileHooksCalled(['validate']);
   }
 
   /**
diff --git a/core/modules/file/tests/src/Kernel/ValidateTest.php b/core/modules/file/tests/src/Kernel/ValidateTest.php
index 3e9eb07a8ae552019c2783626ce70bd8e92ee2dd..f1547d62efaa86414d9eb2952aadc4cd6d18036c 100644
--- a/core/modules/file/tests/src/Kernel/ValidateTest.php
+++ b/core/modules/file/tests/src/Kernel/ValidateTest.php
@@ -35,4 +35,23 @@ public function testCallerValidation() {
     $this->assertFileHooksCalled(['validate']);
   }
 
+  /**
+   * Tests hard-coded security check in file_validate().
+   */
+  public function testInsecureExtensions() {
+    $file = $this->createFile('test.php', 'Invalid PHP');
+
+    // Test that file_validate() will check for insecure extensions by default.
+    $errors = file_validate($file, []);
+    $this->assertEquals('For security reasons, your upload has been rejected.', $errors[0]);
+    $this->assertFileHooksCalled(['validate']);
+    file_test_reset();
+
+    // Test that the 'allow_insecure_uploads' is respected.
+    $this->config('system.file')->set('allow_insecure_uploads', TRUE)->save();
+    $errors = file_validate($file, []);
+    $this->assertEmpty($errors);
+    $this->assertFileHooksCalled(['validate']);
+  }
+
 }
diff --git a/core/modules/jsonapi/src/Controller/TemporaryJsonapiFileFieldUploader.php b/core/modules/jsonapi/src/Controller/TemporaryJsonapiFileFieldUploader.php
index 8ebeaf870df8cb7d471c57e3f7a71468724b93e4..a887aa7c96b5d4f58be65bb8ad4f7c3bee50cec7 100644
--- a/core/modules/jsonapi/src/Controller/TemporaryJsonapiFileFieldUploader.php
+++ b/core/modules/jsonapi/src/Controller/TemporaryJsonapiFileFieldUploader.php
@@ -386,26 +386,42 @@ protected function validate(FileInterface $file, array $validators) {
    *   The prepared/munged filename.
    */
   protected function prepareFilename($filename, array &$validators) {
-    if (!empty($validators['file_validate_extensions'][0])) {
-      // If there is a file_validate_extensions validator and a list of
-      // valid extensions, munge the filename to protect against possible
-      // malicious extension hiding within an unknown file type. For example,
-      // "filename.html.foo".
-      $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
-    }
-
-    // Rename potentially executable files, to help prevent exploits (i.e. will
-    // rename filename.php.foo and filename.php to filename.php.foo.txt and
-    // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads'
-    // evaluates to TRUE.
-    if (!$this->systemFileConfig->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
-      // The destination filename will also later be used to create the URI.
-      $filename .= '.txt';
-
-      // The .txt extension may not be in the allowed list of extensions. We
-      // have to add it here or else the file upload will fail.
+    //  Don't rename if 'allow_insecure_uploads' evaluates to TRUE.
+    if (!$this->systemFileConfig->get('allow_insecure_uploads')) {
       if (!empty($validators['file_validate_extensions'][0])) {
-        $validators['file_validate_extensions'][0] .= ' txt';
+        // If there is a file_validate_extensions validator and a list of
+        // valid extensions, munge the filename to protect against possible
+        // malicious extension hiding within an unknown file type. For example,
+        // "filename.html.foo".
+        $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
+      }
+
+      // Rename potentially executable files, to help prevent exploits (i.e.
+      // will rename filename.php.foo and filename.php to filename._php._foo.txt
+      // and filename._php.txt, respectively).
+      if (preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename)) {
+        // If the file will be rejected anyway due to a disallowed extension, it
+        // should not be renamed; rather, we'll let file_validate_extensions()
+        // reject it below.
+        $passes_validation = FALSE;
+        if (!empty($validators['file_validate_extensions'][0])) {
+          $file = File::create([]);
+          $file->setFilename($filename);
+          $passes_validation = empty(file_validate_extensions($file, $validators['file_validate_extensions'][0]));
+        }
+        if (empty($validators['file_validate_extensions'][0]) || $passes_validation) {
+          if (substr($filename, -4) != '.txt') {
+            // The destination filename will also later be used to create the URI.
+            $filename .= '.txt';
+          }
+          $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0] ?? '');
+
+          // The .txt extension may not be in the allowed list of extensions. We
+          // have to add it here or else the file upload will fail.
+          if (!empty($validators['file_validate_extensions'][0])) {
+            $validators['file_validate_extensions'][0] .= ' txt';
+          }
+        }
       }
     }
 
diff --git a/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php b/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
index 211257efff5e39c0871664772e0b548a83890474..b63c030c6e5284ec64b5e777ebc93c301c74d6ab 100644
--- a/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/FileUploadTest.php
@@ -621,11 +621,11 @@ public function testFileUploadMaliciousExtension() {
     $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
     // The filename is not munged because .txt is added and it is a known
     // extension to apache.
-    $expected = $this->getExpectedDocument(1, 'example.php.txt', TRUE);
+    $expected = $this->getExpectedDocument(1, 'example.php_.txt', TRUE);
     // Override the expected filesize.
     $expected['data']['attributes']['filesize'] = strlen($php_string);
     $this->assertResponseData($expected, $response);
-    $this->assertFileExists('public://foobar/example.php.txt');
+    $this->assertFileExists('public://foobar/example.php_.txt');
 
     // Add php as an allowed format. Allow insecure uploads still being FALSE
     // should still not allow this. So it should still have a .txt extension
@@ -635,11 +635,11 @@ public function testFileUploadMaliciousExtension() {
     $this->rebuildAll();
 
     $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
-    $expected = $this->getExpectedDocument(2, 'example_2.php.txt', TRUE);
+    $expected = $this->getExpectedDocument(2, 'example_2.php_.txt', TRUE);
     // Override the expected filesize.
     $expected['data']['attributes']['filesize'] = strlen($php_string);
     $this->assertResponseData($expected, $response);
-    $this->assertFileExists('public://foobar/example_2.php.txt');
+    $this->assertFileExists('public://foobar/example_2.php_.txt');
     $this->assertFileNotExists('public://foobar/example_2.php');
 
     // Allow .doc file uploads and ensure even a mis-configured apache will not
@@ -659,6 +659,45 @@ public function testFileUploadMaliciousExtension() {
     $this->assertFileExists('public://foobar/example_3.php_.doc');
     $this->assertFileNotExists('public://foobar/example_3.php.doc');
 
+    // Test that a dangerous extension such as .php is munged even if it is in
+    // the list of allowed extensions.
+    $this->field->setSetting('file_extensions', 'doc php')->save();
+    $this->rebuildAll();
+
+    // Test using a masked exploit file.
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php.doc"']);
+    // The filename is munged.
+    $expected = $this->getExpectedDocument(4, 'example_4.php_.doc', TRUE);
+    // Override the expected filesize.
+    $expected['data']['attributes']['filesize'] = strlen($php_string);
+    // The file mime should be 'application/msword'.
+    $expected['data']['attributes']['filemime'] = 'application/msword';
+    $this->assertResponseData($expected, $response);
+    $this->assertFileExists('public://foobar/example_4.php_.doc');
+    $this->assertFileNotExists('public://foobar/example_4.php.doc');
+
+    // Dangerous extensions are munged even when all extensions are allowed.
+    $this->field->setSetting('file_extensions', '')->save();
+    $this->rebuildAll();
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_5.php.png"']);
+    $expected = $this->getExpectedDocument(5, 'example_5.php_.png_.txt', TRUE);
+    // Override the expected filesize.
+    $expected['data']['attributes']['filesize'] = strlen($php_string);
+    // The file mime should also now be text.
+    $expected['data']['attributes']['filemime'] = 'text/plain';
+    $this->assertResponseData($expected, $response);
+    $this->assertFileExists('public://foobar/example_5.php_.png_.txt');
+
+    // Dangerous extensions are munged if is renamed to end in .txt.
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_6.cgi.png.txt"']);
+    $expected = $this->getExpectedDocument(6, 'example_6.cgi_.png_.txt', TRUE);
+    // Override the expected filesize.
+    $expected['data']['attributes']['filesize'] = strlen($php_string);
+    // The file mime should also now be text.
+    $expected['data']['attributes']['filemime'] = 'text/plain';
+    $this->assertResponseData($expected, $response);
+    $this->assertFileExists('public://foobar/example_6.cgi_.png_.txt');
+
     // Now allow insecure uploads.
     \Drupal::configFactory()
       ->getEditable('system.file')
@@ -668,14 +707,14 @@ public function testFileUploadMaliciousExtension() {
     $this->field->setSetting('file_extensions', '')->save();
     $this->rebuildAll();
 
-    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php"']);
-    $expected = $this->getExpectedDocument(4, 'example_4.php', TRUE);
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
+    $expected = $this->getExpectedDocument(7, 'example_7.php', TRUE);
     // Override the expected filesize.
     $expected['data']['attributes']['filesize'] = strlen($php_string);
     // The file mime should also now be PHP.
     $expected['data']['attributes']['filemime'] = 'application/x-httpd-php';
     $this->assertResponseData($expected, $response);
-    $this->assertFileExists('public://foobar/example_4.php');
+    $this->assertFileExists('public://foobar/example_7.php');
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
index 619af887f5bae2904a73f3b0615200d04488868d..a6355c827500b81494177144af6391bbbd97bbc4 100644
--- a/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
@@ -517,11 +517,11 @@ public function testFileUploadMaliciousExtension() {
     $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']);
     // The filename is not munged because .txt is added and it is a known
     // extension to apache.
-    $expected = $this->getExpectedNormalizedEntity(1, 'example.php.txt', TRUE);
+    $expected = $this->getExpectedNormalizedEntity(1, 'example.php_.txt', TRUE);
     // Override the expected filesize.
     $expected['filesize'][0]['value'] = strlen($php_string);
     $this->assertResponseData($expected, $response);
-    $this->assertFileExists('public://foobar/example.php.txt');
+    $this->assertFileExists('public://foobar/example.php_.txt');
 
     // Add php as an allowed format. Allow insecure uploads still being FALSE
     // should still not allow this. So it should still have a .txt extension
@@ -531,11 +531,11 @@ public function testFileUploadMaliciousExtension() {
     $this->refreshTestStateAfterRestConfigChange();
 
     $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']);
-    $expected = $this->getExpectedNormalizedEntity(2, 'example_2.php.txt', TRUE);
+    $expected = $this->getExpectedNormalizedEntity(2, 'example_2.php_.txt', TRUE);
     // Override the expected filesize.
     $expected['filesize'][0]['value'] = strlen($php_string);
     $this->assertResponseData($expected, $response);
-    $this->assertFileExists('public://foobar/example_2.php.txt');
+    $this->assertFileExists('public://foobar/example_2.php_.txt');
     $this->assertFileNotExists('public://foobar/example_2.php');
 
     // Allow .doc file uploads and ensure even a mis-configured apache will not
@@ -555,6 +555,45 @@ public function testFileUploadMaliciousExtension() {
     $this->assertFileExists('public://foobar/example_3.php_.doc');
     $this->assertFileNotExists('public://foobar/example_3.php.doc');
 
+    // Test that a dangerous extension such as .php is munged even if it is in
+    // the list of allowed extensions.
+    $this->field->setSetting('file_extensions', 'doc php')->save();
+    $this->refreshTestStateAfterRestConfigChange();
+
+    // Test using a masked exploit file.
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php.doc"']);
+    // The filename is munged.
+    $expected = $this->getExpectedNormalizedEntity(4, 'example_4.php_.doc', TRUE);
+    // Override the expected filesize.
+    $expected['filesize'][0]['value'] = strlen($php_string);
+    // The file mime should be 'application/msword'.
+    $expected['filemime'][0]['value'] = 'application/msword';
+    $this->assertResponseData($expected, $response);
+    $this->assertFileExists('public://foobar/example_4.php_.doc');
+    $this->assertFileNotExists('public://foobar/example_4.php.doc');
+
+    // Dangerous extensions are munged even when all extensions are allowed.
+    $this->field->setSetting('file_extensions', '')->save();
+    $this->rebuildAll();
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_5.php.png"']);
+    $expected = $this->getExpectedNormalizedEntity(5, 'example_5.php_.png_.txt', TRUE);
+    // Override the expected filesize.
+    $expected['filesize'][0]['value'] = strlen($php_string);
+    // The file mime should also now be text.
+    $expected['filemime'][0]['value'] = 'text/plain';
+    $this->assertResponseData($expected, $response);
+    $this->assertFileExists('public://foobar/example_5.php_.png_.txt');
+
+    // Dangerous extensions are munged if is renamed to end in .txt.
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_6.cgi.png.txt"']);
+    $expected = $this->getExpectedNormalizedEntity(6, 'example_6.cgi_.png_.txt', TRUE);
+    // Override the expected filesize.
+    $expected['filesize'][0]['value'] = strlen($php_string);
+    // The file mime should also now be text.
+    $expected['filemime'][0]['value'] = 'text/plain';
+    $this->assertResponseData($expected, $response);
+    $this->assertFileExists('public://foobar/example_6.cgi_.png_.txt');
+
     // Now allow insecure uploads.
     \Drupal::configFactory()
       ->getEditable('system.file')
@@ -564,14 +603,14 @@ public function testFileUploadMaliciousExtension() {
     $this->field->setSetting('file_extensions', '')->save();
     $this->refreshTestStateAfterRestConfigChange();
 
-    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php"']);
-    $expected = $this->getExpectedNormalizedEntity(4, 'example_4.php', TRUE);
+    $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_7.php"']);
+    $expected = $this->getExpectedNormalizedEntity(7, 'example_7.php', TRUE);
     // Override the expected filesize.
     $expected['filesize'][0]['value'] = strlen($php_string);
     // The file mime should also now be PHP.
     $expected['filemime'][0]['value'] = 'application/x-httpd-php';
     $this->assertResponseData($expected, $response);
-    $this->assertFileExists('public://foobar/example_4.php');
+    $this->assertFileExists('public://foobar/example_7.php');
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php b/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php
index 991f9263c9bde6481f8b964895be453af7438599..26f54f97fcc1e59c3da5bb48ff29e9f6129ebd97 100644
--- a/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/NameMungingTest.php
@@ -7,28 +7,51 @@
 /**
  * Tests filename munging and unmunging.
  *
+ * Checks performed, relying on 2 <= strlen('foo') <= 5.
+ *
+ * - testMunging()
+ *   - (name).foo.txt -> (name).foo_.txt when allows_insecure_uploads === 0
+ * - testMungeBullByte()
+ *   - (name).foo\0.txt -> (name).foo_.txt regardless of allows_insecure_uploads
+ * - testMungeIgnoreInsecure()
+ *   - (name).foo.txt unmodified when allows_insecure_uploads === 1
+ * - testMungeIgnoreAllowedExtensions()
+ *   - (name).FOO.txt -> (name).FOO when allowing 'foo'.
+ *   - (name).foo.txt -> (name).foo.txt when allowing 'FOO'.
+ * - testMungeUnsafe()
+ *   - (name).php.txt -> (name).php_.txt even when allowing 'php txt'
+ *   - (name).php.txt -> (name).php_.txt even when allowing 'php txt'
+ * - testUnMunge()
+ *   - (name).foo.txt -> (unchecked) -> (name).foo.txt after un-munging
+ *
  * @group File
  */
 class NameMungingTest extends FileTestBase {
 
   /**
+   * An extension to be used as forbidden during munge operations.
+   *
    * @var string
    */
   protected $badExtension;
 
   /**
+   * The name of a file with a bad extension, after munging.
+   *
    * @var string
    */
   protected $name;
 
   /**
+   * The name of a file with an upper-cased bad extension, after munging.
+   *
    * @var string
    */
   protected $nameWithUcExt;
 
   protected function setUp(): void {
     parent::setUp();
-    $this->badExtension = 'php';
+    $this->badExtension = 'foo';
     $this->name = $this->randomMachineName() . '.' . $this->badExtension . '.txt';
     $this->nameWithUcExt = $this->randomMachineName() . '.' . strtoupper($this->badExtension) . '.txt';
   }
@@ -78,6 +101,18 @@ public function testMungeIgnoreAllowedExtensions() {
     $this->assertSame($munged_name, $this->name);
   }
 
+  /**
+   * Tests unsafe extensions are always munged by file_munge_filename().
+   */
+  public function testMungeUnsafe() {
+    $prefix = $this->randomMachineName();
+    $name = "$prefix.php.txt";
+    // Put the php extension in the allowed list, but since it is in the unsafe
+    // extension list, it should still be munged.
+    $munged_name = file_munge_filename($name, 'php txt');
+    $this->assertSame("$prefix.php_.txt", $munged_name);
+  }
+
   /**
    * Ensure that unmunge gets your name back.
    */