diff --git a/core/modules/image/src/Hook/ImageDownloadFileHook.php b/core/modules/image/src/Hook/ImageDownloadFileHook.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b12ebbae3d2bbc17dcd71bd45d94133cc8d8910
--- /dev/null
+++ b/core/modules/image/src/Hook/ImageDownloadFileHook.php
@@ -0,0 +1,191 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image\Hook;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Image\ImageFactory;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\image\Controller\ImageStyleDownloadController;
+use Drupal\image\Plugin\Field\FieldType\ImageItem;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+
+/**
+ * Implements hook_file_download().
+ */
+#[Hook('file_download')]
+class ImageDownloadFileHook {
+
+  /**
+   * Cache for private default images.
+   *
+   * @var array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
+   */
+  protected array $cachedPrivateDefaultImages;
+
+  public function __construct(
+    #[Autowire(service: 'cache.default')]
+    protected readonly CacheBackendInterface $cache,
+    protected readonly EntityTypeManagerInterface $entityTypeManager,
+    protected readonly EntityRepositoryInterface $entityRepository,
+    protected readonly EntityFieldManagerInterface $entityFieldManager,
+    protected readonly AccountInterface $currentUser,
+    protected readonly ImageFactory $imageFactory,
+    protected readonly ConfigFactoryInterface $configFactory,
+    protected readonly ModuleHandlerInterface $moduleHandler,
+  ) {}
+
+  /**
+   * Implements hook_file_download().
+   */
+  public function __invoke(string $uri): array|int|null {
+    $path = StreamWrapperManager::getTarget($uri);
+    // Private file access for image style derivatives.
+    if (str_starts_with($path, 'styles/')) {
+      $args = explode('/', $path);
+      // Discard "styles", style name, and scheme from the path
+      $args = array_slice($args, 3);
+      // Then the remaining parts are the path to the image.
+      $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
+      // Check that the file exists and is an image.
+      $image = $this->imageFactory->get($uri);
+      if ($image->isValid()) {
+        // If the image style converted the extension, it has been added to the
+        // original file, resulting in filenames like image.png.jpeg. So to find
+        // the actual source image, we remove the extension and check if that
+        // image exists.
+        if (!file_exists($original_uri)) {
+          $converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
+          if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
+            // The converted file does exist, use it as the source.
+            $original_uri = $converted_original_uri;
+          }
+        }
+        // Check the permissions of the original to grant access to this image.
+        $headers = $this->moduleHandler->invokeAll('file_download', [$original_uri]);
+        // Confirm there's at least one module granting access and none denying access.
+        if (!empty($headers) && !in_array(-1, $headers)) {
+          return [
+            // Send headers describing the image's size, and MIME-type.
+            'Content-Type' => $image->getMimeType(),
+            'Content-Length' => $image->getFileSize(),
+          ];
+        }
+      }
+      return -1;
+    }
+    // If it is the sample image we need to grant access.
+    $samplePath = $this->configFactory->get('image.settings')->get('preview_image');
+    if ($path === $samplePath) {
+      $image = $this->imageFactory->get($samplePath);
+      return [
+        // Send headers describing the image's size, and MIME-type.
+        'Content-Type' => $image->getMimeType(),
+        'Content-Length' => $image->getFileSize(),
+      ];
+    }
+
+    // Private file access for image fields' default images. Default images are
+    // displayed as a fallback when an image is not uploaded to an image field.
+    if (str_starts_with($path, ImageItem::DEFAULT_IMAGE_DIRECTORY . DIRECTORY_SEPARATOR)) {
+      $image = $this->imageFactory->get($uri);
+      if ($image->isValid()) {
+        $private_default_images = $this->getPrivateDefaultImages();
+        if (isset($private_default_images[$uri])) {
+          foreach ($private_default_images[$uri] as $field_definition) {
+            $access_control_handler = $this->entityTypeManager->getAccessControlHandler($field_definition->getTargetEntityTypeId());
+            // As long as the user has view access to at least one of the fields,
+            // that uses this image as a default, we can exit this foreach loop,
+            // and grant access.
+            if ($access_control_handler->fieldAccess('view', $field_definition)) {
+              return [
+                // Send headers describing the image's size, and MIME-type.
+                'Content-Type' => $image->getMimeType(),
+                'Content-Length' => $image->getFileSize(),
+                // By not explicitly setting them here, this uses normal Drupal
+                // Expires, Cache-Control and ETag headers to prevent proxy or
+                // browser caching of private images.
+              ];
+            }
+          }
+        }
+      }
+      return -1;
+    }
+
+    return NULL;
+  }
+
+  /**
+   * Returns the mapping of private default images to field definitions.
+   *
+   * @return array<string, \Drupal\Core\Field\FieldDefinitionInterface[]>
+   *   An associative array where keys are private image URIs and values are
+   *   arrays of field definitions that reference these images as defaults.
+   */
+  protected function getPrivateDefaultImages(): array {
+    if (!isset($this->cachedPrivateDefaultImages)) {
+      $cid = 'image:default_images';
+      if ($cache = $this->cache->get($cid)) {
+        $this->cachedPrivateDefaultImages = $cache->data;
+      }
+      else {
+        // Save a map of all default image UUIDs and their corresponding field
+        // definitions for quick lookup.
+        $private_default_images = [];
+        $field_map = $this->entityFieldManager->getFieldMapByFieldType('image');
+        $cache_tags = [
+          'image_default_images',
+          'entity_field_info',
+        ];
+        foreach ($field_map as $entity_type_id => $fields) {
+          // Do not filter field storages by uri_scheme, as file entities may
+          // change regardless of the field storage configuration.
+          $field_storages = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);
+          foreach ($fields as $field_name => $field_info) {
+            // First, check if the default image is set on the field storage.
+            $uri_from_storage = NULL;
+            $file_uuid = $field_storages[$field_name]->getSetting('default_image')['uuid'];
+            if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
+              /** @var \Drupal\file\FileInterface $file */
+              $uri_from_storage = $file->getFileUri();
+              $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
+            }
+
+            foreach ($field_info['bundles'] as $bundle) {
+              $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
+              $default_uri = $uri_from_storage;
+              $file_uuid = $field_definition->getSetting('default_image')['uuid'];
+              // If the default image is overridden in the field definition, use
+              // that instead of the one set on the field storage.
+              if ($file_uuid && $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid)) {
+                /** @var \Drupal\file\FileInterface $file */
+                $default_uri = $file->getFileUri();
+                $cache_tags = Cache::mergeTags($cache_tags, $file->getCacheTags());
+              }
+              // Finally, if a private default image URI was found,
+              // add it to the list.
+              if ($default_uri && StreamWrapperManager::getScheme($default_uri) === 'private') {
+                $private_default_images[$default_uri][] = $field_definition;
+              }
+            }
+          }
+        }
+        // Cache the default image list.
+        $this->cachedPrivateDefaultImages = $private_default_images;
+        $this->cache->set($cid, $private_default_images, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
+      }
+    }
+    return $this->cachedPrivateDefaultImages;
+  }
+
+}
diff --git a/core/modules/image/src/Hook/ImageHooks.php b/core/modules/image/src/Hook/ImageHooks.php
index 2d6af269220b0e0e0ddf20c52ba3ad93d1422ab4..06d2af69cb44e2103b93de1b69afd179cc595f61 100644
--- a/core/modules/image/src/Hook/ImageHooks.php
+++ b/core/modules/image/src/Hook/ImageHooks.php
@@ -8,7 +8,6 @@
 use Drupal\field\FieldStorageConfigInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\file\FileInterface;
-use Drupal\image\Controller\ImageStyleDownloadController;
 use Drupal\Core\StreamWrapper\StreamWrapperManager;
 use Drupal\Core\Url;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -166,62 +165,6 @@ public function theme() : array {
     ];
   }
 
-  /**
-   * Implements hook_file_download().
-   *
-   * Control the access to files underneath the styles directory.
-   */
-  #[Hook('file_download')]
-  public function fileDownload($uri): array|int|null {
-    $path = StreamWrapperManager::getTarget($uri);
-    // Private file access for image style derivatives.
-    if (str_starts_with($path, 'styles/')) {
-      $args = explode('/', $path);
-      // Discard "styles", style name, and scheme from the path
-      $args = array_slice($args, 3);
-      // Then the remaining parts are the path to the image.
-      $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
-      // Check that the file exists and is an image.
-      $image = \Drupal::service('image.factory')->get($uri);
-      if ($image->isValid()) {
-        // If the image style converted the extension, it has been added to the
-        // original file, resulting in filenames like image.png.jpeg. So to find
-        // the actual source image, we remove the extension and check if that
-        // image exists.
-        if (!file_exists($original_uri)) {
-          $converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
-          if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
-            // The converted file does exist, use it as the source.
-            $original_uri = $converted_original_uri;
-          }
-        }
-        // Check the permissions of the original to grant access to this image.
-        $headers = \Drupal::moduleHandler()->invokeAll('file_download', [$original_uri]);
-        // Confirm there's at least one module granting access and none denying
-        // access.
-        if (!empty($headers) && !in_array(-1, $headers)) {
-          return [
-                // Send headers describing the image's size, and MIME-type.
-            'Content-Type' => $image->getMimeType(),
-            'Content-Length' => $image->getFileSize(),
-          ];
-        }
-      }
-      return -1;
-    }
-    // If it is the sample image we need to grant access.
-    $samplePath = \Drupal::config('image.settings')->get('preview_image');
-    if ($path === $samplePath) {
-      $image = \Drupal::service('image.factory')->get($samplePath);
-      return [
-            // Send headers describing the image's size, and MIME-type.
-        'Content-Type' => $image->getMimeType(),
-        'Content-Length' => $image->getFileSize(),
-      ];
-    }
-    return NULL;
-  }
-
   /**
    * Implements hook_file_move().
    */
diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
index 2cae36dc6cf68bd7829f400aad363a0e8fd8a761..038f8e080cabb51206affbcef47a0ad1a40e634d 100644
--- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
+++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
@@ -60,6 +60,11 @@ class ImageItem extends FileItem {
 
   use LoggerChannelTrait;
 
+  /**
+   * The default image directory.
+   */
+  public const string DEFAULT_IMAGE_DIRECTORY = 'default_images';
+
   /**
    * {@inheritdoc}
    */
@@ -459,7 +464,7 @@ protected function defaultImageForm(array &$element, array $settings) {
       '#title' => $this->t('Image'),
       '#description' => $this->t('Image to be shown if no image is uploaded.'),
       '#default_value' => $fids,
-      '#upload_location' => $settings['uri_scheme'] . '://default_images/',
+      '#upload_location' => $settings['uri_scheme'] . '://' . static::DEFAULT_IMAGE_DIRECTORY . '/',
       '#element_validate' => [
         '\Drupal\file\Element\ManagedFile::validateManagedFile',
         [static::class, 'validateDefaultImageForm'],
diff --git a/core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml b/core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1f42e8e780609690758c4593501fd3f07b080165
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test/config/schema/image_field_display_test.schema.yml
@@ -0,0 +1,7 @@
+field.field.*.*.*.third_party.image_field_display_test:
+  type: mapping
+  label: 'Image field display test settings'
+  mapping:
+    access_denied:
+      type: boolean
+      label: 'Access denied'
diff --git a/core/modules/image/tests/modules/image_field_display_test/image_field_display_test.info.yml b/core/modules/image/tests/modules/image_field_display_test/image_field_display_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a7952d0183fc05d6471a6204c73132533035cee7
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test/image_field_display_test.info.yml
@@ -0,0 +1,5 @@
+name: 'Image field display test for default images in private file storage'
+type: module
+description: 'Provides an entity field access hook to deny view access to a field for the current user.'
+package: Testing
+version: VERSION
diff --git a/core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php b/core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e03d58d57f1be17b518769bedcc5f667df2e2d28
--- /dev/null
+++ b/core/modules/image/tests/modules/image_field_display_test/src/Hook/ImageFieldDisplayTestHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image_field_display_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Field\FieldConfigInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Implements hook_entity_field_access().
+ */
+#[Hook('entity_field_access')]
+class ImageFieldDisplayTestHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  public function __invoke(string $operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL): AccessResultInterface {
+    if ($operation === 'view'
+        && $field_definition instanceof FieldConfigInterface
+        && $field_definition->getThirdPartySetting('image_field_display_test', 'access_denied', FALSE)) {
+      return AccessResult::forbidden()->addCacheableDependency($field_definition);
+    }
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
index efeacd6ff1651ec3ff49fc7620bbda0da1627cfd..3a410bec76ec9c394195c4ad180fe06bce70adca 100644
--- a/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
+++ b/core/modules/image/tests/src/Functional/ImageFieldDisplayTest.php
@@ -7,7 +7,9 @@
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\StreamWrapper\StreamWrapperManager;
 use Drupal\Core\Url;
+use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\file\FileInterface;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\Tests\TestFileCreationTrait;
@@ -567,7 +569,7 @@ public function testImageFieldDefaultImage(): void {
     // Create an image field that uses the private:// scheme and test that the
     // default image works as expected.
     $private_field_name = $this->randomMachineName();
-    $this->createImageField($private_field_name, 'node', 'article', ['uri_scheme' => 'private']);
+    $private_field = $this->createImageField($private_field_name, 'node', 'article', ['uri_scheme' => 'private']);
     // Add a default image to the new field.
     $edit = [
       // Get the path of the 'image-test.gif' file.
@@ -583,6 +585,7 @@ public function testImageFieldDefaultImage(): void {
     $private_field_storage = FieldStorageConfig::loadByName('node', $private_field_name);
     $default_image = $private_field_storage->getSetting('default_image');
     $file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $default_image['uuid']);
+    $this->assertInstanceOf(FileInterface::class, $file);
 
     $this->assertEquals('private', StreamWrapperManager::getScheme($file->getFileUri()), 'Default image uses private:// scheme.');
     $this->assertTrue($file->isPermanent(), 'The default image status is permanent.');
@@ -606,6 +609,48 @@ public function testImageFieldDefaultImage(): void {
     // Default private image should be displayed when no user supplied image
     // is present.
     $this->assertSession()->responseContains($default_output);
+
+    // Check if the default image set for the field storage can be downloaded;
+    // i.e., not just the HTML markup.
+    $this->container->get('module_installer')->install(['image_field_display_test']);
+    $private_default_image_url = $file->createFileUrl();
+    $this->drupalGet($private_default_image_url);
+    $this->assertSession()->statusCodeEquals(200);
+    // Now disable access to the field and check that the
+    // same user now receives a 403 Access Denied.
+    $private_field->setThirdPartySetting('image_field_display_test', 'access_denied', TRUE)->save();
+    $this->drupalGet($private_default_image_url);
+    $this->assertSession()->statusCodeEquals(403);
+
+    // Override the default image with the field configuration for
+    // the next access check.
+    $edit = [
+      // Get the path of the 'image-test-transparent-out-of-range.gif' file.
+      'files[settings_default_image_uuid]' => $this->container->get('file_system')->realpath($images[3]->uri),
+      'settings[default_image][alt]' => $alt,
+      'settings[default_image][title]' => $title,
+    ];
+    $this->drupalGet("admin/structure/types/manage/article/fields/node.article.$private_field_name");
+    $this->submitForm($edit, 'Save');
+    // Clear the cache and reload the field configuration
+    // to retrieve the UUID of the default image file.
+    $this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
+    $private_field = FieldConfig::loadByName('node', 'article', $private_field_name);
+    $file_uuid = $private_field->getSetting('default_image')['uuid'];
+    $file = $this->container->get('entity.repository')->loadEntityByUuid('file', $file_uuid);
+    $this->assertInstanceOf(FileInterface::class, $file);
+
+    // Check if the default image overridden by the field configuration
+    // can be downloaded.
+    $private_default_image_url = $file->createFileUrl();
+    $private_field->setThirdPartySetting('image_field_display_test', 'access_denied', FALSE)->save();
+    $this->drupalGet($private_default_image_url);
+    $this->assertSession()->statusCodeEquals(200);
+    // Now disable access to the field and check that the
+    // same user now receives a 403 Access Denied.
+    $private_field->setThirdPartySetting('image_field_display_test', 'access_denied', TRUE)->save();
+    $this->drupalGet($private_default_image_url);
+    $this->assertSession()->statusCodeEquals(403);
   }
 
 }