If you’ve just enabled image uploads or badge layouts in CiviCRM on a Drupal 10 site and the KCFinder image browser popup either hangs, shows a 403 error, or displays “Unknown error” instead of your files, you’re likely hitting one of three separate bugs that commonly affect Composer-based CiviCRM installs. Here’s how to diagnose and fix each one.

Issue 1: KCFinder hangs and times out (Maximum execution time exceeded)

If clicking an image field opens a popup that eventually fails with “Fatal error: Maximum execution time of 120 seconds exceeded” pointing at kcfinder/integration/civicrm.php, the cause is almost always the same thing: KCFinder’s bundled CiviCRM integration only knows how to look for civicrm.config.php or civicrm.standalone.php. Composer-based Drupal installs use civicrm.settings.php instead, usually at web/sites/default/civicrm.settings.php, which sits in a completely different branch of the folder structure. KCFinder’s lookup function searches upward through parent folders looking for a filename that will never be found there, until PHP’s execution time limit kills the request.

The fix

Open vendor/civicrm/civicrm-packages/kcfinder/integration/civicrm.php and back it up first. Then find the findConfigFile() function and replace it with a version that checks for civicrm.settings.php directly, and guard it with function_exists() so it can’t throw a “Cannot redeclare” fatal error if it runs more than once per request:

if (!function_exists('findConfigFile')) {
  function findConfigFile(string $search_path): string {
    $settings_candidate = $search_path . '/../../../../../web/sites/default/civicrm.settings.php';
    if (file_exists($settings_candidate)) {
      return $settings_candidate;
    }
    while ($search_path) {
      foreach(['civicrm.config.php', 'civicrm.standalone.php', 'civicrm.settings.php'] as $config_filename) {
        $config_file_candidate = $search_path . DIRECTORY_SEPARATOR . $config_filename;
        if (file_exists($config_file_candidate)) {
          return $config_file_candidate;
        }
      }
      $search_path = dirname($search_path);
    }
    throw new \Exception('KCFinder could not find a CiviCRM config or settings file');
  }
}

Confirm the relative path before applying this on your own install, since it depends on your folder layout:

realpath --relative-to=vendor/civicrm/civicrm-packages/kcfinder/integration web/sites/default

Issue 2: KCFinder loads but shows no styling, and console shows 403 errors

If the popup opens but looks unstyled and broken, with no working buttons, open the browser console (F12). If you see a string of 403 Forbidden errors for files like js/index.php, js_localize.php, themes/default/js.php and themes/default/css.php, followed by “Refused to apply style” and “Refused to execute script” warnings and an Uncaught ReferenceError, the cause is Drupal’s own security rules.

Drupal’s .htaccess blocks direct execution of PHP files as standard practice, with a single carve-out rule for KCFinder. That carve-out typically only whitelists browse.php and upload.php, but KCFinder also serves its CSS and JavaScript through PHP proxy scripts, which then get blocked along with everything else.

The fix

In web/.htaccess, find the existing KCFinder rewrite rule:

RewriteRule ^libraries/civicrm/packages/kcfinder/(browse|upload)\.php$ - [L]

Broaden it to allow any PHP file under that path:

RewriteRule ^libraries/civicrm/packages/kcfinder/.*\.php$ - [L]

Issue 3: The file listing shows “Unknown error”

If the popup now loads with its proper toolbar and icons, but the file listing panel just says “Unknown error” instead of showing your images, check the Network tab in DevTools for the request that fires when KCFinder loads its file list (it looks something like browse.php?type=images&act=init). Look at the raw response body. If you see PHP warnings or notices printed as plain text above what should be clean JSON, that’s the cause: the warnings corrupt the response, and KCFinder’s JavaScript can’t parse it, so it falls back to a generic error.

This is common if your PHP environment displays warnings and notices on screen, since CiviCRM core and Drupal core both throw the occasional non-fatal warning that becomes a real problem here.

The fix

Add error suppression to the very top of vendor/civicrm/civicrm-packages/kcfinder/integration/civicrm.php, right after the opening PHP tag:

error_reporting(E_ERROR | E_PARSE);
ini_set('display_errors', '0');

A note on permanence

All three of these fixes live inside files that are normally managed for you: two are inside vendor/, which Composer can overwrite on update, and one is Drupal’s own .htaccess. If you run composer update on civicrm/civicrm-packages, or regenerate your .htaccess as part of a deployment process, these changes can be lost. The robust way to make them permanent is to turn them into proper patches using cweagans/composer-patches, referenced in your composer.json, so they reapply automatically on every install or update.

It’s also worth noting that the underlying gap in KCFinder’s CiviCRM integration, where it doesn’t know how to find civicrm.settings.php, is still present in the current upstream civicrm-packages repository at the time of writing. If you run into this, it’s worth raising as an issue there, since it affects any Composer-based Drupal install using CiviCRM’s image browser, not just one specific setup.