From 367e57e7bc22a6079d20762d9b1e9e43d4355016 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Sun, 4 Feb 2024 11:56:13 +0000
Subject: [PATCH] Issue #2481349 by mfb, dagmar, jofitz, smustgrave, vasi,
 neclimdul, ziomizar, quietone, _utsavsharma, littlepixiez, xjm, fgm, Todd
 Zebert, hass, dawehner, heddn, webchick, catch: Prevent the use of
 placeholders that cannot be converted into strings when creating logs

---
 .../Drupal/Core/Logger/LogMessageParser.php   | 11 +++-
 .../dblog/tests/src/Kernel/DbLogTest.php      | 15 ++++++
 .../Core/Logger/LogMessageParserTest.php      | 53 ++++++++++++++-----
 3 files changed, 63 insertions(+), 16 deletions(-)

diff --git a/core/lib/Drupal/Core/Logger/LogMessageParser.php b/core/lib/Drupal/Core/Logger/LogMessageParser.php
index 3d94f6b77b6e..76fc951d01a1 100644
--- a/core/lib/Drupal/Core/Logger/LogMessageParser.php
+++ b/core/lib/Drupal/Core/Logger/LogMessageParser.php
@@ -29,9 +29,16 @@ public function parseMessagePlaceholders(&$message, array &$context) {
           $key = '@' . $key;
         }
       }
+      // To be considered a valid placeholder, the key should be in
+      // \Drupal\Component\Render\FormattableMarkup style and the variable
+      // should be a string, number, or \Stringable object. For historical
+      // reasons, Boolean and NULL placeholders are also allowed; NULL
+      // placeholders are deprecated and may throw an error in the future.
+      // @see https://www.drupal.org/node/3318826
       if (!empty($key) && ($key[0] === '@' || $key[0] === '%' || $key[0] === ':')) {
-        // The key is now in \Drupal\Component\Render\FormattableMarkup style.
-        $variables[$key] = $variable;
+        if (is_scalar($variable) || is_null($variable) || $variable instanceof \Stringable) {
+          $variables[$key] = $variable;
+        }
       }
     }
 
diff --git a/core/modules/dblog/tests/src/Kernel/DbLogTest.php b/core/modules/dblog/tests/src/Kernel/DbLogTest.php
index 59283e795f3f..a36cc29783eb 100644
--- a/core/modules/dblog/tests/src/Kernel/DbLogTest.php
+++ b/core/modules/dblog/tests/src/Kernel/DbLogTest.php
@@ -60,6 +60,21 @@ function (callable $hook, string $module) use (&$implementation_count) {
     $this->assertEquals(1, $cron_count, "Cron added $cron_count of 1 new log entries");
   }
 
+  /**
+   * Tests that only valid placeholders are stored in the variables column.
+   */
+  public function testInvalidPlaceholders() {
+    \Drupal::logger('my_module')->warning('Hello @string @array @object', ['@string' => '', '@array' => [], '@object' => new \stdClass()]);
+    $variables = \Drupal::database()
+      ->select('watchdog', 'w')
+      ->fields('w', ['variables'])
+      ->orderBy('wid', 'DESC')
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+    $this->assertSame(serialize(['@string' => '']), $variables);
+  }
+
   /**
    * Runs cron and returns number of new log entries.
    *
diff --git a/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php b/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php
index a9a71766c180..537d727fb6cc 100644
--- a/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php
+++ b/core/tests/Drupal/Tests/Core/Logger/LogMessageParserTest.php
@@ -4,6 +4,7 @@
 
 namespace Drupal\Tests\Core\Logger;
 
+use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Logger\LogMessageParser;
 use Drupal\Tests\UnitTestCase;
 
@@ -40,38 +41,62 @@ public function testParseMessagePlaceholders(array $value, array $expected) {
    */
   public function providerTestParseMessagePlaceholders() {
     return [
-      // PSR3 only message.
-      [
+      'PSR3-style placeholder' => [
         ['message' => 'User {username} created', 'context' => ['username' => 'Dries']],
         ['message' => 'User @username created', 'context' => ['@username' => 'Dries']],
       ],
-      // PSR3 style mixed in a format_string style message.
-      [
+      'PSR3- and FormattableMarkup-style placeholders' => [
         ['message' => 'User {username} created @time', 'context' => ['username' => 'Dries', '@time' => 'now']],
         ['message' => 'User @username created @time', 'context' => ['@username' => 'Dries', '@time' => 'now']],
       ],
-      // format_string style message only.
-      [
+      'FormattableMarkup-style placeholder' => [
         ['message' => 'User @username created', 'context' => ['@username' => 'Dries']],
         ['message' => 'User @username created', 'context' => ['@username' => 'Dries']],
       ],
-      // Message without placeholders but wildcard characters.
-      [
+      'Wildcard characters' => [
         ['message' => 'User W-\\};~{&! created @', 'context' => ['' => '']],
         ['message' => 'User W-\\};~{&! created @', 'context' => []],
       ],
-      // Message with double PSR3 style messages.
-      [
+      'Multiple PSR3-style placeholders' => [
         ['message' => 'Test {with} two {{encapsuled}} strings', 'context' => ['with' => 'together', 'encapsuled' => 'awesome']],
         ['message' => 'Test @with two {@encapsuled} strings', 'context' => ['@with' => 'together', '@encapsuled' => 'awesome']],
       ],
-
-      // Test removal of unexpected placeholders like ! while allowed
-      // placeholders beginning with @, % and : are preserved.
-      [
+      'Disallowed placeholder' => [
         ['message' => 'Test placeholder with :url and old !bang parameter', 'context' => [':url' => 'https://example.com', '!bang' => 'foo bar']],
         ['message' => 'Test placeholder with :url and old !bang parameter', 'context' => [':url' => 'https://example.com']],
       ],
+      'Stringable object placeholder' => [
+        ['message' => 'object @b', 'context' => ['@b' => new FormattableMarkup('convertible', [])]],
+        ['message' => 'object @b', 'context' => ['@b' => 'convertible']],
+      ],
+      'Non-placeholder context value' => [
+        ['message' => 'message', 'context' => ['not_a_placeholder' => new \stdClass()]],
+        ['message' => 'message', 'context' => []],
+      ],
+      'Non-stringable array placeholder' => [
+        ['message' => 'array @a', 'context' => ['@a' => []]],
+        ['message' => 'array @a', 'context' => []],
+      ],
+      'Non-stringable object placeholder' => [
+        ['message' => 'object @b', 'context' => ['@b' => new \stdClass()]],
+        ['message' => 'object @b', 'context' => []],
+      ],
+      'Non-stringable closure placeholder' => [
+        ['message' => 'closure @c', 'context' => ['@c' => function () {}]],
+        ['message' => 'closure @c', 'context' => []],
+      ],
+      'Non-stringable resource placeholder' => [
+        ['message' => 'resource @r', 'context' => ['@r' => fopen('php://memory', 'r+')]],
+        ['message' => 'resource @r', 'context' => []],
+      ],
+      'Non-stringable placeholder is not the first placeholder' => [
+        ['message' => 'mixed @a @b @c', 'context' => ['@a' => 123, '@b' => [1], '@c' => TRUE]],
+        ['message' => 'mixed @a @b @c', 'context' => ['@a' => 123, '@c' => TRUE]],
+      ],
+      'NULL and Boolean placeholders are considered stringable' => [
+        ['message' => 'mixed @a @b @c', 'context' => ['@a' => NULL, '@b' => TRUE, '@c' => FALSE]],
+        ['message' => 'mixed @a @b @c', 'context' => ['@a' => NULL, '@b' => TRUE, '@c' => FALSE]],
+      ],
     ];
   }
 
-- 
GitLab