The Security Team has approved this issue for public handling.

Problem/Motivation

In some scenarios it is possible to access the /core/install.php even when a site is already installed. This can lead to regenerating the admin user. Original raised with the security team but advised its fine to publicly post.

This is possible because this code here in core/includes/install.core.inc

  if ($install_state['config_verified'] && empty($task)) {
    if (count($kernel->getConfigStorage()->listAll())) {
      $task = NULL;
      throw new AlreadyInstalledException($container->get('string_translation'));
    }
  }

Checks that $task is empty, and if not it assumes its in the new installation process and let's the install continue without throwing the AlreadyInstalledException.
When translations are pending import, the task is set as `install_import_translations` because this code will result in TRUE:

$needs_translations = $locale_module_installed && ((count($install_state['translations']) > 1 && !empty($install_state['parameters']['langcode']) && $install_state['parameters']['langcode'] != 'en') || \Drupal::languageManager()->isMultilingual());

Steps to reproduce

You can see this issue by:

  1. Create a drupal site
  2. Install the site and enable translations
  3. Deploy with any importing translation failure
  4. Go to /core/install.php and reinstall Drupal, setting the admin user and password to whatever you want
  5. Go to the site on completion and login as your new admin user. The existing database is not wiped, but the admin user is overridden

Note that the Contact module must also be enabled on the site otherwise the install process triggers a fatal error stopping the above from happening. So probably many sites don't have this potential issue as a result.

Update @szeidler 29.10.2025: If the Contact module is disabled, the installer triggers a fatal error, but the admin user data is still replaced. So the attacker can reset the password with his newly set email address on user #1 and gains full control over the Drupal installation from the user pespective.

A simpler way to reproduce on any site programmatically without having to reproduce a more complex failure:

  1. Run drush state:set install_task 'install_import_translations' --input-format=string
  2. Go to /core/install.php and reinstall Drupal, setting the admin user and password to whatever you want
  3. Go to the site on completion and login as your new admin user. The existing database is not wiped, but the admin user is overridden

Proposed resolution

Double-check that Drupal is not yet installed.

For those coming across this same issue, if you get any import translation failure, you can manually run the following to re-protect your site:
drush state:set install_task 'done' --input-format=string

Remaining tasks

Provide an additional check

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

Prevent re-installation of Drupal

Issue fork drupal-3455853

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

scott_euser created an issue. See original summary.

scott_euser’s picture

Status: Active » Needs review

Could use some suggestions on how to add tests for this (if its possible for the install)

scott_euser’s picture

Issue summary: View changes
scott_euser’s picture

Issue summary: View changes
smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Appears to have relevant test failures.

Also MR should be updated for 11.x vs 11.0.x

scott_euser’s picture

Issue summary: View changes
Status: Needs work » Needs review

Okay I updated the approach to check for the existence of user 1 already.

I updated the issue summary with an explanation on how to simply reproduce this on any Drupal site via Drush (of course because its programmatically it then does not have the risk, but hopefully its helpful for testing and moving this issue forward).

The tests will continue to fail though because the problem is that the installation tests actually do as the issue here defines, with different causes.

  1. InstallerTestBase::setUp() calls parent ::setUp()
  2. parent ::setUp() calls ::installDrupal() which is overridden by InstallerTestBase and installs drupal
  3. InstallerTestBase::setUp() then runs checks that Drupal can be installed and the tests fail if not BUT Drupal is already installed because ::installDrupal() is run.

So really its testing that Drupal can be reinstalled when its already installed which actually seems like it might not be correct?

I did add a test which shows with a simple state change, the installer can be re-run without the code change (which eventually leads to user 1 getting overridden).

Given this is going to affect so many tests (36 tests) because of the above, I wonder if the simpler thing to do is just to allow a $setting variable like $settings['disable_installer'] = TRUE; and if that would be easier to get in. Essentially I do not want to put lots of effort into something that will likely never get merged.

Ultimately I want to be able to prevent existing sites from getting user 1 overridden as to me that's a high risk issue, so being able to disable the installer would similarly achieve that. I can of course manually set the state to done to avoid the issue but a future failure can cause the state to change back as its possible that state is wiped in which case the installer goes through the steps again and if it has e.g. import translation failures, it will not consider that step done.

quietone’s picture

Version: 11.0.x-dev » 11.x-dev
larowlan’s picture

Issue tags: +Security improvements

Clarifying that this was logged privately to the security team who approved for it to be public

smustgrave’s picture

Status: Needs review » Needs work

So definitely seems like something to fix. Posted about it in #needs-review-queue-initiative and as @larowlan mentioned was originally security issue made public.

Before adding $settings['disable_installer'] = TRUE is there any benefit to having this as FALSE? If there are scenarios then I think the setting makes sense but if something that should always be TRUE maybe we just don't allow it?

szeidler’s picture

We're starting to see automated requests trying to exploit this problem.

Even though I'm getting a Fatal error, if the contact module is not installed: the user information + site information + default language can be changed. The attacker can login with his new credentials on user 1. I have tested it with Drupal 10.4.8.

I had cases where it was updating the password. Sometimes not. But the email changes, so I'm able to reset my password as an attacker and gain access that way.

I can imagine a mass vulnerable scanning on a large scale could find sites with a translation import failure at the time of scan.

oily’s picture

Re: #7

Given this is going to affect so many tests (36 tests) because of the above, I wonder if the simpler thing to do is just to allow a $setting variable like $settings['disable_installer'] = TRUE; and if that would be easier to get in. Essentially I do not want to put lots of effort into something that will likely never get merged.

It looks like whatever fix is decided on it will definitely get merged. So should perhaps consider more involved fixes as it will be worth putting in the work. Though perhaps something quick and dirty that closes up the security hole like $settings['disable_installer'] = TRUE is good as a short-term measure while the code inside core is fixed to prevent this at source.

szeidler’s picture

Issue summary: View changes
szeidler’s picture

I did some further investigation about my recent comment. Sometimes the set password works. sometimes not. I found a pattern here.

  1. Run drush state:set install_task 'install_import_translations' --input-format=string
  2. Go to /core/install.php and reinstall Drupal, setting the admin user and password to whatever you want
  3. Go to the site on completion and login as your new admin user. The existing database is not wiped, but the admin user is overridden

On the first exploitation and if I install/uninstall a module before opening /core/install.php the entered user/password combination will be directly usable for login.

If I don't install/uninstall a module before then the user/password combination will not directly be possible to use. But since the username + email was overwritten I can easily reset my password via the "Forgot password form" and gain access like this.

mcdruid’s picture

Unpublishing this public issue while we review whether to continue handling it in public or reconsider and move back to the private tracker.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

xjm’s picture

Issue summary: View changes
mingsong’s picture

Thanks for the merge request.

I tested it with a brand new D11 installation (11.3.3)

Prerequisites:

  1. Drupal core version: 11.3.3
  2. Apply the patch from MR !8463 mergeable

Steps to Reproduce:

  1. Open a browser and navigate to /core/install.php to begin a fresh installation.
  2. Follow the normal installation steps:
    1. Choose language (e.g., English)
    2. Choose profile (e.g., Standard)
    3. Set up database credentials
    4. The installer will proceed to the "Install site" step (the batch progress bar where it installs modules). Wait for this to finish 100%.
    5. The installer will automatically redirect me to the "Configure site" page (the form where I set the Site Name, Admin Username, Password, etc.).
    6. DO NOT submit the form. Stop here.

      Simply Refresh my browser page (or manually navigate to /core/install.php in the address bar).

Result:

Expected Behavior (Without MR / Drupal Core standard): The installer recognizes that the last completed task is install_install_profile and immediately resumes by re-rendering the "Configure site" form so I can finish my installation.

Actual Behavior (With MR !8463 mergeable applied): I will be immediately blocked with an AlreadyInstalledException ("Drupal already installed"). I am now completely locked out of the installer and cannot finish configuring my site.

scott_euser’s picture

Status: Needs work » Needs review

Good spot! I added a check to the MR to verify that the configuration page has been saved (and if it hasn't yet, allow access to it still)

scott_euser changed the visibility of the branch 3455853-prevent-reinstall to hidden.

scott_euser’s picture

Targeted main now and got tests passing.

scott_euser changed the visibility of the branch main to hidden.

scott_euser changed the visibility of the branch 11.x to hidden.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs tests

Removing tests tag as that's present in https://git.drupalcode.org/issue/drupal-3455853/-/jobs/9216990

I followed the steps best I could and from what I can tell with the MR I cannot install Drupal again. Going on a limb that this may be ready.

xjm’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs manual testing

I followed the steps best I could and from what I can tell with the MR I cannot install Drupal again. Going on a limb that this may be ready.

I think we need more detailed, step-by-step documentation of manual testing than that, especially given the severity of both this issue and the earlier regression. There are two scenarios we need to test manually:

  1. The steps mentioned in #18 to ensure that regression is resolved. We expect the results with both HEAD and the MR to be the same for this case
  2. The steps described in the IS (before and after), to ensure that the conflation of "failed translation import" with "open installer" is gone and the security issue is resolved. We expect HEAD and the MR to have different results for this case.

Please document your manual testing steps for both scenarios in detail, both the before and after.

xjm’s picture

Issue tags: +DrupalSouth 2026