Throw an understandable exception when there is an attempt to load config entities with disallowed characters

Created on 19 September 2024, about 1 month ago
Updated 20 September 2024, about 1 month ago

There is no validation of encoding on any of the configuration object types, which throws unhandled exception.

Problem/Motivation

Configuration system handles look up of objects defined in Drupal.
Objects are stored in "config" database table, with object stored in "name" field (eg node.type.article, views.view.articles, user.role.anonymous, etc)

- "name" field is defined as "varchar_ascii" in Drupal, and varchar(255) with collation ascii_general_ci in Database
- "name" is used to look up routes, node types, view names, user roles, etc etc

To resolve URL "node/add/article" route is matched to node/add/{node_type}
readMultiple() is called in core/lib/Drupal/Core/Config/DatabaseStorage.php to check configuration object {node_type} exists

There is no validation of encoding on any of the configuration object types, which throws unhandled exception:

The website encountered an unexpected error. Try again later.

Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[HY000]: General error: 1267 Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8mb4_general_ci,COERCIBLE) for operation '=': SELECT "name", "data" FROM "config" WHERE "collection" = :collection AND "name" IN ( :names__0 ); Array ( [:collection] => [:names__0] => node.type.ั…ะฐะบะตั€ ) in Drupal\Core\Config\DatabaseStorage->readMultiple() (line 110 of core/lib/Drupal/Core/Config/DatabaseStorage.php).

This leads to unnecessary hits of database which can lead to server outage.
The error message is not handled and white screen of death is displayed.

This issue affects all routes of configuration objects, example:

- /node/add/รถรผรค
- /media/add/รถรผรค
- /admin/structure/views/view/รถรผรค

Steps to reproduce

Navigate to a route and insert non-ASCII values into URL:
- /node/add/ั‚ะตัั‚
- /media/add/ั‚ะตัั‚
- /admin/structure/views/view/ั‚ะตัั‚
- /views/ajax?view_name=view_ั‚ะตัั‚&view_display_id=page_1&_drupal_ajax=1

Proposed resolution

add validation before running SQL query

Remaining tasks

- tests

User interface changes

- none

Introduced terminology

- none

API changes

- none

Data model changes

- none

Release notes snippet

- Added validation before looking up configuration objects (Unhandled exception when looking up a configuration objects by name which contains non-ASCII characters)

Note: There are a lot of configuration fields which are collated to ascii_general_ci but mostly don't have any validation before a non ASCII-values is being queried or inserted which throws an unhandled exception. Some of the issues will be resolved by this patch.

๐Ÿ“Œ Task
Status

Needs work

Version

11.0 ๐Ÿ”ฅ

Component
Configurationย  โ†’

Last updated 3 days ago

Created by

๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @jannakha
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

    jannakha โ†’ changed the visibility of the branch 3475540-unhandled-exception-ascii to hidden.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!
  • Status changed to Needs review about 1 month ago
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

    before I start writing tests - please review/comment/provide suggestions to the solution!
    Thanks!

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!
  • I don't believe this is a bug and I think it is the caller's responsibility to filter input. By that I mean that each use-case needs fixing separately.

    What does this have to do with security?

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia VladimirAus Brisbane, Australia
  • Indeed but I would say that particular one is a node system bug. Any code that loads configuration objects should understand the rules: that their keys are ASCII only.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

    @cilefen
    Re: "caller's responsibility to filter input" - it's a lookup into config database which doesn't validate non-ascii input, there are levels of abstractions between config object and node/webform/view/or other caller which will make it quite hard to validate it on per-caller basis.

    * there's a case-by-case fix which doesn't fix much: https://www.drupal.org/project/drupal/issues/3457963 ๐Ÿ› Validate view_name in ajaxView() Needs work

    It was discovered during a pen test and flagged as a potential security issue since:
    - This leads to unnecessary hits of database which can lead to server outage.
    - The error message is not handled and white screen of death is displayed.

    examples:
    https://events.drupal.org/node/add/ั‚ะตัั‚
    https://www.cyber.gov.au/node/add/ั‚ะตัั‚

    Stack trace of node/add/{node_type} and each other config has its own trace:

    #0 /web/core/lib/Drupal/Core/Database/Connection.php(883): Drupal\mysql\Driver\Database\mysql\ExceptionHandler->handleExecutionException(Object(PDOException), Object(Drupal\Core\Database\StatementWrapperIterator), Array, Array)
    #1 /web/core/lib/Drupal/Core/Config/DatabaseStorage.php(119): Drupal\Core\Database\Connection->query('SELECT [name], ...', Array, Array)
    #2 /web/core/lib/Drupal/Core/Config/CachedStorage.php(95): Drupal\Core\Config\DatabaseStorage->readMultiple(Array)
    #3 /web/core/lib/Drupal/Core/Config/ConfigFactory.php(165): Drupal\Core\Config\CachedStorage->readMultiple(Array)
    #4 /web/core/lib/Drupal/Core/Config/ConfigFactory.php(136): Drupal\Core\Config\ConfigFactory->doLoadMultiple(Array)
    #5 /web/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php(181): Drupal\Core\Config\ConfigFactory->loadMultiple(Array)
    #6 /web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(312): Drupal\Core\Config\Entity\ConfigEntityStorage->doLoadMultiple(Array)
    #7 /web/core/lib/Drupal/Core/Entity/EntityRepository.php(183): Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array)
    #8 /web/core/lib/Drupal/Core/Entity/EntityRepository.php(175): Drupal\Core\Entity\EntityRepository->getCanonicalMultiple('node_type', Array, Array)
    #9 /web/core/lib/Drupal/Core/ParamConverter/EntityConverter.php(134): Drupal\Core\Entity\EntityRepository->getCanonical('node_type', '\xD0\xB4\xD1\x8B\xD0\xBB\xD0\xB2\xD0\xBE\xD0\xB0\xD0\xB4\xD1...', Array)
    #10 /web/core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php(100): Drupal\Core\ParamConverter\EntityConverter->convert('\xD0\xB4\xD1\x8B\xD0\xBB\xD0\xB2\xD0\xBE\xD0\xB0\xD0\xB4\xD1...', Array, 'node_type', Array)
    #11 /web/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php(45): Drupal\Core\ParamConverter\ParamConverterManager->convert(Array)
    #12 /web/core/lib/Drupal/Core/Routing/Router.php(270): Drupal\Core\Routing\Enhancer\ParamConversionEnhancer->enhance(Array, Object(Symfony\Component\HttpFoundation\Request))
    #13 /web/core/lib/Drupal/Core/Routing/Router.php(150): Drupal\Core\Routing\Router->applyRouteEnhancers(Array, Object(Symfony\Component\HttpFoundation\Request))
    #14 /web/core/lib/Drupal/Core/Routing/AccessAwareRouter.php(90): Drupal\Core\Routing\Router->matchRequest(Object(Symfony\Component\HttpFoundation\Request))
    #15 /vendor/symfony/http-kernel/EventListener/RouterListener.php(105): Drupal\Core\Routing\AccessAwareRouter->matchRequest(Object(Symfony\Component\HttpFoundation\Request))
    #16 [internal function]: Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest(Object(Symfony\Component\HttpKernel\Event\RequestEvent), 'kernel.request', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher))
    
  • Pipeline finished with Success
    about 1 month ago
    Total: 2048s
    #287230
  • Issue was unassigned.
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!
  • Maybe throw a specific exception for this?

  • Even an InvalidArgument exception explaining that only ASCII is allowed would be clear to callers.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States smustgrave

    +1 for throwing an exception mentioned in #13

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

    so I've added the exception at Drupal\Core\Config\DatabaseStorage->readMultiple() level, but it's not being caught anywhere in the stack and each stack trace is different for different config types (see below).

    Please advise/recommend where/how exception should be thrown/caught.

    IMHO handling it on Drupal\Core\Config\DatabaseStorage->readMultiple() without additional exceptions is sufficient as it's such an edge case which is used for exploitation/attack only (there's already check for if (empty($names)) which doesn't throw any exceptions).

    Here's a stack trace for: /node/add/ั‚ะตัั‚

    The website encountered an unexpected error. Try again later.
    
    InvalidArgumentException: The array contains invalid values. in Drupal\Core\Config\DatabaseStorage->readMultiple() (line 116 of core/lib/Drupal/Core/Config/DatabaseStorage.php).
    Drupal\Core\Config\CachedStorage->readMultiple(Array) (Line: 165)
    Drupal\Core\Config\ConfigFactory->doLoadMultiple(Array) (Line: 136)
    Drupal\Core\Config\ConfigFactory->loadMultiple(Array) (Line: 181)
    Drupal\Core\Config\Entity\ConfigEntityStorage->doLoadMultiple(Array) (Line: 312)
    Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array) (Line: 183)
    Drupal\Core\Entity\EntityRepository->getCanonicalMultiple('node_type', Array, Array) (Line: 175)
    Drupal\Core\Entity\EntityRepository->getCanonical('node_type', 'ั‚ะตัั‚', Array) (Line: 134)
    Drupal\Core\ParamConverter\EntityConverter->convert('ั‚ะตัั‚', Array, 'node_type', Array) (Line: 100)
    Drupal\Core\ParamConverter\ParamConverterManager->convert(Array) (Line: 45)
    Drupal\Core\Routing\Enhancer\ParamConversionEnhancer->enhance(Array, Object) (Line: 270)
    Drupal\Core\Routing\Router->applyRouteEnhancers(Array, Object) (Line: 150)
    Drupal\Core\Routing\Router->matchRequest(Object) (Line: 90)
    Drupal\Core\Routing\AccessAwareRouter->matchRequest(Object) (Line: 105)
    Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest(Object, 'kernel.request', Object)
    call_user_func(Array, Object, 'kernel.request', Object) (Line: 111)
    

    here's a stack for: /admin/structure/views/view/ั‚ะตัั‚

    The website encountered an unexpected error. Try again later.
    
    InvalidArgumentException: The array contains invalid values. in Drupal\Core\Config\DatabaseStorage->readMultiple() (line 116 of core/lib/Drupal/Core/Config/DatabaseStorage.php).
    Drupal\Core\Config\CachedStorage->readMultiple(Array) (Line: 165)
    Drupal\Core\Config\ConfigFactory->doLoadMultiple(Array) (Line: 136)
    Drupal\Core\Config\ConfigFactory->loadMultiple(Array) (Line: 181)
    Drupal\Core\Config\Entity\ConfigEntityStorage->doLoadMultiple(Array) (Line: 312)
    Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array) (Line: 487)
    Drupal\Core\Config\Entity\ConfigEntityStorage->loadMultipleOverrideFree(Array) (Line: 478)
    Drupal\Core\Config\Entity\ConfigEntityStorage->loadOverrideFree('ั‚ะตัั‚') (Line: 80)
    Drupal\Core\ParamConverter\AdminPathConfigEntityConverter->convert('ั‚ะตัั‚', Array, 'view', Array) (Line: 64)
    Drupal\views_ui\ParamConverter\ViewUIConverter->convert('ั‚ะตัั‚', Array, 'view', Array) (Line: 100)
    Drupal\Core\ParamConverter\ParamConverterManager->convert(Array) (Line: 45)
    Drupal\Core\Routing\Enhancer\ParamConversionEnhancer->enhance(Array, Object) (Line: 270)
    Drupal\Core\Routing\Router->applyRouteEnhancers(Array, Object) (Line: 150)
    Drupal\Core\Routing\Router->matchRequest(Object) (Line: 90)
    Drupal\Core\Routing\AccessAwareRouter->matchRequest(Object) (Line: 105)
    Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest(Object, 'kernel.request', Object)
    call_user_func(Array, Object, 'kernel.request', Object) (Line: 111)
    

    Here's stack trace for a contrib module: /webform/ั‚ะตัั‚:

    The website encountered an unexpected error. Try again later.
    
    InvalidArgumentException: The array contains invalid values. in Drupal\Core\Config\DatabaseStorage->readMultiple() (line 116 of core/lib/Drupal/Core/Config/DatabaseStorage.php).
    Drupal\Core\Config\CachedStorage->readMultiple(Array) (Line: 165)
    Drupal\Core\Config\ConfigFactory->doLoadMultiple(Array) (Line: 136)
    Drupal\Core\Config\ConfigFactory->loadMultiple(Array) (Line: 181)
    Drupal\Core\Config\Entity\ConfigEntityStorage->doLoadMultiple(Array) (Line: 312)
    Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array) (Line: 183)
    Drupal\Core\Entity\EntityRepository->getCanonicalMultiple('webform', Array, Array) (Line: 175)
    Drupal\Core\Entity\EntityRepository->getCanonical('webform', 'ั‚ะตัั‚', Array) (Line: 134)
    Drupal\Core\ParamConverter\EntityConverter->convert('ั‚ะตัั‚', Array, 'webform', Array) (Line: 100)
    Drupal\Core\ParamConverter\ParamConverterManager->convert(Array) (Line: 45)
    Drupal\Core\Routing\Enhancer\ParamConversionEnhancer->enhance(Array, Object) (Line: 270)
    Drupal\Core\Routing\Router->applyRouteEnhancers(Array, Object) (Line: 150)
    Drupal\Core\Routing\Router->matchRequest(Object) (Line: 90)
    Drupal\Core\Routing\AccessAwareRouter->matchRequest(Object) (Line: 105)
    Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest(Object, 'kernel.request', Object)
    call_user_func(Array, Object, 'kernel.request', Object) (Line: 111)
  • Indeed, what I am suggesting is a developer experience improvement that would highlight the precise problem of invalid input to the function. It is up to the callers to provide valid input in the first place or to handle the exception.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States smustgrave

    Will also need test coverage.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia cafuego

    I will add a dissenting opinion, just to be helpful. Rather than be limited to us-ascii, Drupal should handle UTF8 throughout. That way, users and developers would be able to properly use Drupal in their local language.

  • Status changed to Needs work about 1 month ago
  • @cafuego That is a separate matter. The reasons for ASCII are from #1923406: Use ASCII character set on alphanumeric fields so we can index all 255 characters โ†’ and I suggest creating a new issue if you want to challenge those decisions in a way that works across all supported database engines.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

    patch of MR9542 @ 265e30ba9c9e65396211044895ec0c1e50c52449 for temporary fix

  • First commit to issue fork.
  • Pipeline finished with Failed
    28 days ago
    Total: 271s
    #292065
  • Pipeline finished with Success
    28 days ago
    Total: 3290s
    #292085
  • ๐Ÿ‡ฎ๐Ÿ‡ณIndia arunkumark Coimbatore

    Added test for the Special character and additional check added into the Configuration exist method.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium herved

    Interesting, I found some incoming requests to our production going to /filter/tips/๏ฝก๏ฝก๏ฝก, which is accessible as anonymous.
    This throws such errors as well.

  • This needs work based on the plan for this issue, which is to throw an understandable exception.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany tobiasb Berlin

    Such request are done by spambots, which triggers a false-positive in your app-monitoring software. So I am happy, with a notfound exception, but not with any other which triggers a false-positive problem.

  • See comment #16. I believe this component should throw a specific exception when there is invalid input and the callers must be fixed to send only valid input.

  • ๐Ÿ‡ฉ๐Ÿ‡ชGermany tobiasb Berlin

    @cilefen

    The caller are spambots. Spambots does not do what we want. ;-)

    I mean do we have issue like:

    Why does this not work?

    $config = \Drupal::configFactory()->getEditable('รœber.settings');
    

    or

    drush cget 'รœber.settings'
    

    The patch does what would happen, when you only use ASCII or postgres (?). Page 404, so I am happy. :D

    The exception DatabaseExceptionWrapper in the new test are not thrown or should not. So try/catch can be removed.

    I would replace it with just a request to /node/add/รœber which should return a 404.
    With postgres this should already throw a not found exception.

    And because we do not use native typehints to force config name is string. the $value should not be null in mb_check_encoding (deprecated).

  • Configuration object names must be ASCII only. Other input is invalid. In my opinion the better developer experience is to throw an exception when there is invalid input than to silently do nothing. Calling functions should handle the exception.

  • The only way to have a consistent experience across database engines is to throw an exception on invalid input.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia jannakha Brisbane!

    If exception is to be thrown, consider:
    where the exception should be thrown:
    - at DB level
    - at config level
    - at entity level
    - at routing level

    where exception should be handled:
    - at config level
    - at entity level
    - at routing level
    - at request processing level

    In the end, exception would be visible to the client as 404 (as route is not found)

  • Not all situations would be 404s.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson

    One approach I have used is to create an event listener. The event is triggered by a 404. But you set additional condition(s) that must be met, so not just any 404 but one which is caused by x, or y. X or y can include a particular exception occurring.

  • Pipeline finished with Failed
    21 days ago
    Total: 234s
    #298354
  • Pipeline finished with Failed
    21 days ago
    Total: 2013s
    #298363
  • Pipeline finished with Failed
    21 days ago
    Total: 643s
    #298377
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson

    If anyone believes that this issue should be fixed by presenting a 404 or other 4xx status code to the user with custom message and possibly also a log message, please create a child issue.

    I would suggest the following steps:

    1. Create a unique Exception e.g. one that extends the InvalidArgumentException class
    2. Create an event listener that listens for the unique Exception to be thrown
    3. Redirect to a 4xx status code page and display a message
    4. Possibly add a log entry also
  • Pipeline finished with Failed
    21 days ago
    Total: 3604s
    #298514
  • Pipeline finished with Failed
    20 days ago
    Total: 669s
    #299002
  • Pipeline finished with Failed
    20 days ago
    Total: 155s
    #299038
  • Pipeline finished with Failed
    20 days ago
    Total: 599s
    #299051
  • Pipeline finished with Failed
    20 days ago
    Total: 611s
    #299084
  • Pipeline finished with Failed
    20 days ago
    Total: 146s
    #299104
  • Pipeline finished with Success
    20 days ago
    Total: 622s
    #299108
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson

    I have edited the kernel test so it now passes and created instead Functional test coverage. I have reverted DatabaseStorage.php to pre-issue version and found that the Functional test fails. Then checking out the latest version of that file (the latest MR) the tests all pass. So I am flagging 'Needs Review'

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
  • Pipeline finished with Success
    20 days ago
    Total: 1976s
    #299167
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States smustgrave

    Appears to still have open threads to address.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson

    @smustgrave I see several things in the threads. I note this issue โ†’ . However if you search through core for usages of InvalidArgumentException you will find that by far the majority are not extending from the base class. Issue 2654936 concerns the Rules module. In that issue it is stated that not extending from base classes like InvalidArgumentException is 'bad OOP'. However if true then they could have created a child issue: to remove all the unextended uses of InvalidArgumentException in core. I am unaware of any initiatives to do that.

  • Pipeline finished with Success
    13 days ago
    #305570
  • Pipeline finished with Failed
    13 days ago
    Total: 171s
    #305586
  • Pipeline finished with Success
    13 days ago
    Total: 535s
    #305615
  • Pipeline finished with Success
    13 days ago
    Total: 454s
    #305648
  • Pipeline finished with Failed
    13 days ago
    Total: 180s
    #305663
  • Pipeline finished with Failed
    13 days ago
    Total: 728s
    #305672
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson

    After throwing InvalidArgumentException for the DatabaseStorage::exists and DatabaseStorage::read methods, I found that the former breaks an existing kernel assertion/ test. So I have reverted to the existing DatabaseExceptionWrapper class to fix the test. The tests are back to green. Setting back to 'Needs review'.

  • Pipeline finished with Success
    12 days ago
    Total: 624s
    #306254
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom andrew.farquharson
Production build 0.71.5 2024