diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc
index 388796ecca..89ee2e4cc1 100644
--- a/core/modules/system/system.tokens.inc
+++ b/core/modules/system/system.tokens.inc
@@ -19,6 +19,16 @@ function system_token_info() {
     'name' => t("Site information"),
     'description' => t("Tokens for site-wide settings and other global information."),
   ];
+  $types['site-logo'] = [
+    'name' => t('Site logo'),
+    'description' => t('Tokens related to the site logo.'),
+    'needs-data' => 'site',
+  ];
+  $types['site-logo-properties'] = [
+    'name' => t('Site logo properties'),
+    'description' => t('Tokens for site logo properties.'),
+    'needs-data' => 'site-logo',
+  ];
   $types['date'] = [
     'name' => t("Dates"),
     'description' => t("Tokens related to times and dates."),
@@ -61,6 +71,45 @@ function system_token_info() {
     'description' => t("The URL of the site's login page."),
   ];

+  // The [site:logo] token renders the active theme logo. Chained tokens can be
+  // used for rendering specific theme logos or logo properties (URLs) instead.
+  $site['logo'] = [
+    'name' => t('Logo'),
+    'description' => t('The logo of the active theme. Note that the theme may be different when this is used in the administrative interface.'),
+    'type' => 'site-logo',
+  ];
+
+  // Tokens for a specific theme logo.
+  $site_logo['active-theme'] = [
+    'name' => t('Active theme'),
+    'description' => t('The logo of the active theme. Note that the theme may be different when this is used in the administrative interface.'),
+    'type' => 'site-logo-properties',
+  ];
+  $site_logo['default-theme'] = [
+    'name' => t('Default theme'),
+    'description' => t('The logo that is configured for the default theme.'),
+    'type' => 'site-logo-properties',
+  ];
+  // Obtain a list of installed themes and make tokens from them.
+  $themes = \Drupal::service('theme_handler')->listInfo();
+  foreach ($themes as $theme => $info) {
+    if (empty($info->info['hidden'])) {
+      $site_logo['theme-' . $theme] = [
+        'name' => t('@theme', ['@theme' => $info->info['name']]),
+        'description' => t('The logo that is configured for the %theme theme.', [
+          '%theme' => $info->info['name'],
+        ]),
+        'type' => 'site-logo-properties',
+      ];
+    }
+  }
+
+  // Tokens for individual properties for logos.
+  $site_logo_properties['url'] = [
+    'name' => t('URL'),
+    'description' => t('The URL of the logo.'),
+  ];
+
   /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
   $date_formatter = \Drupal::service('date.formatter');

@@ -95,6 +144,8 @@ function system_token_info() {
     'types' => $types,
     'tokens' => [
       'site' => $site,
+      'site-logo' => $site_logo,
+      'site-logo-properties' => $site_logo_properties,
       'date' => $date,
     ],
   ];
@@ -173,10 +224,65 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe
           $bubbleable_metadata->addCacheableDependency($result);
           $replacements[$original] = $result->getGeneratedUrl();
           break;
+
+        case 'logo':
+          $replacements[$original] = _system_tokens_get_logo_rendered('active-theme', $bubbleable_metadata);
+          break;
       }
     }
+
+    // Detect chained tokens ([site:logo:?]).
+    if ($logo_tokens = $token_service->findWithPrefix($tokens, 'logo')) {
+      $replacements += $token_service->generate('site-logo', $logo_tokens, [], $options, $bubbleable_metadata);
+    }
   }
+  elseif ($type == 'site-logo') {
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'active-theme':
+        case 'default-theme':
+          $replacements[$original] = _system_tokens_get_logo_rendered($name, $bubbleable_metadata);
+          break;
+      }
+    }
+
+    if ($logo_tokens = $token_service->findWithPrefix($tokens, 'active-theme')) {
+      $replacements += $token_service->generate('site-logo-properties', $logo_tokens, ['theme' => 'active-theme'], $options, $bubbleable_metadata);
+    }
+    if ($logo_tokens = $token_service->findWithPrefix($tokens, 'default-theme')) {
+      $replacements += $token_service->generate('site-logo-properties', $logo_tokens, ['theme' => 'default-theme'], $options, $bubbleable_metadata);
+    }
+
+    // List installed themes.
+    $themes = \Drupal::service('theme_handler')->listInfo();
+    $installed_themes = array_keys($themes);

+    // Detect direct tokens ([site:logo:theme-bartik]).
+    foreach ($tokens as $name => $original) {
+      // Strip 'theme-' to get the theme name, but do not strip 'active-theme'.
+      $name = str_starts_with($name, 'theme-') ? substr($name, 6) : $name;
+      if (in_array($name, $installed_themes)) {
+        $replacements[$original] = _system_tokens_get_logo_rendered($name, $bubbleable_metadata);
+      }
+    }
+
+    // Detect chained tokens ([site:logo:theme-bartik:?]).
+    foreach ($installed_themes as $installed_theme) {
+      if ($created_tokens = $token_service->findWithPrefix($tokens, 'theme-' . $installed_theme)) {
+        $replacements += $token_service->generate('site-logo-properties', $created_tokens, ['theme' => $installed_theme], $options, $bubbleable_metadata);
+      }
+    }
+  }
+  elseif ($type == 'site-logo-properties' && !empty($data['theme'])) {
+    foreach ($tokens as $name => $original) {
+      switch ($name) {
+        case 'url':
+          $logo_path = _system_tokens_get_logo_path($data['theme'], $bubbleable_metadata);
+          $replacements[$original] = Url::fromUserInput($logo_path, ['absolute' => TRUE])->toString();
+          break;
+      }
+    }
+  }
   elseif ($type == 'date') {
     if (empty($data['date'])) {
       $date = \Drupal::time()->getRequestTime();
@@ -218,3 +324,62 @@ function system_tokens($type, $tokens, array $data, array $options, BubbleableMe

   return $replacements;
 }
+
+/**
+ * Returns the path of the logo for the given theme.
+ *
+ * @see _system_tokens_get_logo_path()
+ *
+ * @param string $theme
+ *   The theme to load the logo for.
+ * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
+ *   The bubbleable metadata to alter in order to cache the token.
+ *
+ * @return mixed
+ *   The path to the logo, NULL if the setting does not exist.
+ */
+function _system_tokens_get_logo_path(string $theme, BubbleableMetadata &$bubbleable_metadata) {
+  if ($theme === 'default-theme') {
+    // Add the cache dependency.
+    $system_theme_config = \Drupal::config('system.theme');
+    $theme = $system_theme_config->get('default');
+    $bubbleable_metadata->addCacheableDependency($system_theme_config);
+  }
+  elseif ($theme === 'active-theme') {
+    $theme = \Drupal::service('theme.manager')->getActiveTheme()->getName();
+    // Needs caching per theme, because it can change.
+    $bubbleable_metadata->addCacheContexts(['theme']);
+  }
+
+  // Because theme_get_setting() can return either the global theme logo or a
+  // theme specific logo, this token depends on both configurations.
+  $global_theme_config = \Drupal::config('system.theme.global');
+  $theme_config = \Drupal::config($theme . '.settings');
+  $bubbleable_metadata->addCacheableDependency($global_theme_config);
+  $bubbleable_metadata->addCacheableDependency($theme_config);
+
+  return theme_get_setting('logo.url', $theme);
+}
+
+/**
+ * Returns an <img> tag for the logo of the given theme.
+ *
+ * @see _system_tokens_get_logo_path()
+ *
+ * @param string $theme
+ *   The theme to load the logo for.
+ * @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
+ *   The bubbleable metadata to alter in order to cache the token.
+ *
+ * @return \Drupal\Component\Render\MarkupInterface
+ *   An HTML <img> tag for the logo.
+ */
+function _system_tokens_get_logo_rendered(string $theme, BubbleableMetadata &$bubbleable_metadata) {
+  $logo_path = _system_tokens_get_logo_path($theme, $bubbleable_metadata);
+  $build = [
+    '#theme' => 'image',
+    '#uri' => $logo_path,
+    '#alt' => t('The site logo'),
+  ];
+  return \Drupal::service('renderer')->renderPlain($build);
+}
diff --git a/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php b/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php
index 2ecff8dcbe..30196a0c81 100644
--- a/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php
+++ b/core/modules/system/tests/src/Kernel/Token/TokenReplaceKernelTest.php
@@ -2,11 +2,11 @@

 namespace Drupal\Tests\system\Kernel\Token;

-use Drupal\Core\Url;
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\Request;

 /**
@@ -149,6 +149,148 @@ public function testSystemSiteTokenReplacement() {
     $this->assertEquals((new BubbleableMetadata())->addCacheContexts(['url.site']), $bubbleable_metadata);
   }

+  /**
+   * Tests the logo tokens for the active and default theme.
+   */
+  public function testSystemSiteLogoTokenReplacement() {
+    // Install Olivero and Claro.
+    \Drupal::service('theme_installer')->install(['olivero', 'claro']);
+    // Prepare instances that we need only once.
+    $theme_initialization = \Drupal::service('theme.initialization');
+    $theme_manager = \Drupal::service('theme.manager');
+    $renderer = \Drupal::service('renderer');
+
+    // Prepare configurations.
+    $global_theme_config = $this->config('system.theme.global');
+    $global_theme_config
+      ->set('logo.path', '/path/to/global_logo.svg')
+      ->set('logo.use_default', FALSE)
+      ->save();
+    $olivero_config = $this->config('olivero.settings');
+    $olivero_config
+      ->set('logo.path', '/path/to/olivero_logo.svg')
+      ->set('logo.use_default', FALSE)
+      ->save();
+    $claro_config = $this->config('claro.settings');
+    $claro_config
+      ->set('logo.path', '/path/to/claro_logo.svg')
+      ->set('logo.use_default', FALSE)
+      ->save();
+
+    // Set Olivero as the default theme.
+    $system_theme_config = $this->config('system.theme');
+    $system_theme_config
+      ->set('default', 'olivero')
+      ->save();
+
+    // And set Claro as the active theme.
+    $active_theme = $theme_initialization->initTheme('claro');
+    $theme_manager->setActiveTheme($active_theme);
+
+    // Render both theme logos.
+    $build = [
+      '#theme' => 'image',
+      '#uri' => theme_get_setting('logo.url', 'olivero'),
+      '#alt' => 'The site logo',
+    ];
+    $olivero_logo = $renderer->renderPlain($build);
+    $build = [
+      '#theme' => 'image',
+      '#uri' => theme_get_setting('logo.url', 'claro'),
+      '#alt' => 'The site logo',
+    ];
+    $claro_logo = $renderer->renderPlain($build);
+
+    $tests = [];
+    $tests['[site:logo]'] = $claro_logo;
+    $tests['[site:logo:active-theme]'] = $claro_logo;
+    $tests['[site:logo:active-theme:url]'] = Url::fromUserInput($claro_config->get('logo.path'), ['absolute' => TRUE])->toString();
+    $tests['[site:logo:default-theme]'] = $olivero_logo;
+    $tests['[site:logo:default-theme:url]'] = Url::fromUserInput($olivero_config->get('logo.path'), ['absolute' => TRUE])->toString();
+
+    $bubbleable_metadata_claro = BubbleableMetadata::createFromObject($global_theme_config)
+      ->addCacheableDependency($claro_config)
+      ->addCacheContexts(['theme']);
+    $bubbleable_metadata_default = BubbleableMetadata::createFromObject($system_theme_config)
+      ->addCacheableDependency($global_theme_config)
+      ->addCacheableDependency($olivero_config);
+
+    $metadata_tests = [];
+    $metadata_tests['[site:logo]'] = $bubbleable_metadata_claro;
+    $metadata_tests['[site:logo:active-theme]'] = $bubbleable_metadata_claro;
+    $metadata_tests['[site:logo:active-theme:url]'] = $bubbleable_metadata_claro;
+    $metadata_tests['[site:logo:default-theme]'] = $bubbleable_metadata_default;
+    $metadata_tests['[site:logo:default-theme:url]'] = $bubbleable_metadata_default;
+
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
+
+    foreach ($tests as $input => $expected) {
+      $bubbleable_metadata = new BubbleableMetadata();
+      $output = $this->tokenService->replace($input, [], [], $bubbleable_metadata);
+      $this->assertEquals($expected, $output, new FormattableMarkup('System site information token %token replaced.', ['%token' => $input]));
+      $this->assertEquals($metadata_tests[$input], $bubbleable_metadata, new FormattableMarkup('Asserting metadata for token %token.', ['%token' => $input]));
+    }
+  }
+
+  /**
+   * Tests the logo tokens for a specific theme.
+   */
+  public function testSystemSiteLogoThemeTokenReplacement() {
+    // Install Olivero and Claro.
+    \Drupal::service('theme_installer')->install(['olivero', 'claro']);
+    // Prepare instances that we need only once.
+    $renderer = \Drupal::service('renderer');
+    // Prepare configurations.
+    // Do not set a logo for Claro, it should fall back to the global logo.
+    $global_theme_config = $this->config('system.theme.global');
+    $global_theme_config
+      ->set('logo.path', '/path/to/global_logo.svg')
+      ->set('logo.use_default', FALSE)
+      ->save();
+    $olivero_config = $this->config('olivero.settings');
+    $olivero_config
+      ->set('logo.path', '/path/to/olivero_logo.svg')
+      ->set('logo.use_default', FALSE)
+      ->save();
+    // Render the Olivero logo.
+    $build = [
+      '#theme' => 'image',
+      '#uri' => theme_get_setting('logo.url', 'olivero'),
+      '#alt' => 'The site logo',
+    ];
+    $olivero_logo = $renderer->renderPlain($build);
+    // Generate and test tokens.
+    $tests = [];
+    $tests['[site:logo:theme-olivero]'] = $olivero_logo;
+    $tests['[site:logo:theme-olivero:url]'] = Url::fromUserInput($olivero_config->get('logo.path'), ['absolute' => TRUE])->toString();
+    $tests['[site:logo:theme-olivero:foo]'] = '[site:logo:theme-olivero:foo]';
+    $tests['[site:logo:theme-not-enabled-theme]'] = '[site:logo:theme-not-enabled-theme]';
+    $tests['[site:logo:theme-not-enabled-theme:url]'] = '[site:logo:theme-not-enabled-theme:url]';
+    $tests['[site:logo:theme-not-enabled-theme:foo]'] = '[site:logo:theme-not-enabled-theme:foo]';
+    $bubbleable_metadata_olivero = BubbleableMetadata::createFromObject($global_theme_config)
+      ->addCacheableDependency($olivero_config);
+    $metadata_tests = [];
+    $metadata_tests['[site:logo:theme-olivero]'] = $bubbleable_metadata_olivero;
+    $metadata_tests['[site:logo:theme-olivero:url]'] = $bubbleable_metadata_olivero;
+    $metadata_tests['[site:logo:theme-olivero:foo]'] = new BubbleableMetadata();
+    $metadata_tests['[site:logo:theme-not-enabled-theme]'] = new BubbleableMetadata();
+    $metadata_tests['[site:logo:theme-not-enabled-theme:url]'] = new BubbleableMetadata();
+    $metadata_tests['[site:logo:theme-not-enabled-theme:foo]'] = new BubbleableMetadata();
+    // Test to make sure that we generated something for each token.
+    $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
+    // Test that the Claro logo has the path to the global logo.
+    $bubbleable_metadata = new BubbleableMetadata();
+    $output = $this->tokenService->replace('[site:logo:theme-claro:url]', [], [], $bubbleable_metadata);
+    $this->assertStringContainsString($global_theme_config->get('logo.path'), $output, 'Token for undefined theme logo falls back to global logo.');
+    foreach ($tests as $input => $expected) {
+      $bubbleable_metadata = new BubbleableMetadata();
+      $output = $this->tokenService->replace($input, [], [], $bubbleable_metadata);
+      $this->assertEquals($expected, $output, new FormattableMarkup('System site information token %token replaced.', ['%token' => $input]));
+      $this->assertEquals($metadata_tests[$input], $bubbleable_metadata, new FormattableMarkup('Asserting metadata for token %token.', ['%token' => $input]));
+    }
+  }
+
   /**
    * Tests the generation of all system date tokens.
    */

