Updates, Composer and Security Advisories

This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production.

This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages.

A Drupal site is never finished: core, contributed modules, themes and the PHP libraries underneath them receive fixes every month, and some of those fixes close security holes. This page explains the whole update chain for a site built the way this sub-section builds the Meridian Museum site: how code is added and updated with Composer, how a patch is applied while waiting for an upstream fix, what drush updatedb and drush deploy do after the code changed, what the Update Status report tells an administrator, what Automatic Updates and Package Manager can and cannot do, how the Drupal Security Team publishes advisories and which releases they cover, how to subscribe to them, and a routine that keeps the site on the safe side without drama.

It was checked on 2026-10-30 against Drupal 11.4.x (11.4.11, released on 2026-10-21, is the latest patch release of its branch), Drush 13 and 14, Composer 2 and PHP 8.3 or later. Dates written as "scheduled" are plans: Drupal 12.0.0 (week of 2026-12-07) and the end of life of Drupal 10 (2026-12-09) are still in the future on that date. The running example is the Meridian Museum: a site with the custom module meridian_collection, the theme meridian and the recipe meridian_events, developed in DDEV and deployed to staging and production. Release cycle, minors and end-of-life dates are explained in Drupal Versions, Release Cycle and Support; this page is about the day-to-day mechanics of staying current.

The model in one paragraph

Drupal code lives in a Composer project. The files composer.json and composer.lock are the truth about which versions of core, modules, themes and libraries the site runs; the web/core, web/modules/contrib and vendor directories are build output created from them. Everything that changes code goes through Composer on a developer or CI machine. The result (the lock file, patches and exported configuration) is committed, and servers rebuild the same code with composer install. After new code is in place, the database must catch up: drush updatedb (or update.php) runs update functions, and drush config:import applies configuration. Security advisories tell you when this loop must run outside the normal calendar.

flowchart LR A[Advisory or monthly window] --> B[composer outdated and composer audit] B --> C[composer update on a branch] C --> D[Test in DDEV and CI] D --> E[Commit composer.json, composer.lock, patches, config] E --> F[Server: composer install] F --> G[drush deploy: updatedb, config:import, cache:rebuild] G --> H[Check status report and logs]

Composer is the only supported way to add or update code

Older Drupal sites were updated by downloading a tarball, by drag-and-drop through FTP, or from the administration interface. That era is over. Three removals, spread over four minors, finished it:

  • Drupal 10.4 and 11.0 removed the pages that installed an extension from a URL or an uploaded archive: /admin/modules/install ("Add new module"), /admin/theme/install ("Add new theme") and /admin/reports/updates/install. The change record says that using Composer is the only way to safely and correctly add new code to a Drupal installation, because an archive unpacked by the UI brings none of the extension’s own dependencies and can be wiped by the next Composer run.

  • Drupal 11.2 removed the user interface that updated modules and themes through authorize.php, deprecated core/authorize.php, the FileTransfer and Updater systems and the related functions and hooks (hook_filetransfer_info, hook_updater_info, hook_verify_update_archive), and renamed the Update module back to Update Status. The deprecated code is scheduled for removal in Drupal 12.0.0, and the change record states that there is no direct replacement.

  • The Update Status module therefore only reports. It tells you that a newer release exists; it never installs it.

The consequence for every role is practical. An editor or site builder cannot upload a module even with the "Administer modules" permission, so the old advice use Extend  Install new module no longer works. An administrator needs shell access (or a CI pipeline) to a machine that has Composer. A developer treats the Composer files as part of the application and reviews them in pull requests like code.

Older books such as Drupal 10 Development Cookbook (2023) and Drupal 10 Masterclass (2023) were written when the Extend page still offered installation from a URL, when drush pm:update and drush dl were being retired but were still talked about, and when Automatic Updates and Project Browser were described as future initiatives. Where those books show an update by archive or by the interface, or where they mention the Update Manager name, the current behaviour is the one described here: the module is called Update Status since 11.2 and only reports.

The Composer project at a glance

A site created as in Installation with Composer and DDEV from the drupal/recommended-project template has this shape (details in Project Layout and Local Development):

{
    "name": "meridian/museum-site",
    "description": "Meridian Museum website",
    "type": "project",
    "license": "GPL-2.0-or-later",
    "minimum-stability": "stable",
    "prefer-stable": true,
    "require": {
        "php": ">=8.3",
        "composer/installers": "^2.3",
        "cweagans/composer-patches": "~2.0",
        "drupal/core-composer-scaffold": "^11.4",
        "drupal/core-project-message": "^11.4",
        "drupal/core-recommended": "^11.4",
        "drupal/admin_toolbar": "^3.0",
        "drupal/pathauto": "^1.0",
        "drupal/search_api": "^1.0",
        "drush/drush": "^13 || ^14"
    },
    "conflict": {
        "drupal/drupal": "*"
    },
    "config": {
        "sort-packages": true,
        "allow-plugins": {
            "composer/installers": true,
            "cweagans/composer-patches": true,
            "drupal/core-composer-scaffold": true,
            "drupal/core-project-message": true,
            "dealerdirect/phpcodesniffer-composer-installer": true
        }
    },
    "extra": {
        "drupal-scaffold": {
            "locations": {
                "web-root": "web/"
            }
        },
        "installer-paths": {
            "web/core": ["type:drupal-core"],
            "web/libraries/{$name}": ["type:drupal-library"],
            "web/modules/contrib/{$name}": ["type:drupal-module"],
            "web/profiles/contrib/{$name}": ["type:drupal-profile"],
            "web/themes/contrib/{$name}": ["type:drupal-theme"],
            "drush/Commands/contrib/{$name}": ["type:drupal-drush"],
            "web/modules/custom/{$name}": ["type:drupal-custom-module"],
            "web/themes/custom/{$name}": ["type:drupal-custom-theme"]
        }
    }
}

The constraints shown for contributed modules are illustrative: use the major branch the project page recommends for your core version. Key points about this file:

  • drupal/core-recommended requires exact versions of the libraries core was tested with (Symfony, Twig, Guzzle and so on), so a site gets the combination the core team tested. From 11.4 it no longer pins Guzzle, Twig or the Symfony polyfills, which gives sites a little more room to take library security fixes ahead of a core release.

  • drupal/core-composer-scaffold copies files that live outside web/core (index.php, .htaccess, robots.txt, update.php, default.settings.php) into web/. A core update can change these scaffold files, so review git diff after an update, particularly .htaccess and robots.txt.

  • drupal/legacy-project is abandoned as of 11.4; do not base new projects on it.

  • Composer 2.2 and later refuse to run a plugin that is not listed in config.allow-plugins, which is why the scaffold, installers and patch plugins are listed.

  • composer.lock is committed. Servers run composer install, never composer update.

Constraints: what composer require writes and what composer update may do

A constraint in composer.json is a range; composer.lock records the exact version inside the range that was installed. composer update moves the lock file forward inside the ranges; composer require changes the ranges.

Constraint Meaning Use it for

^11.4

>=11.4.0 <12.0.0. Allows the next Drupal minors in 11.x (11.5, 11.6, …​) but never 12.

Core packages when you want to follow minors. This is what composer require writes by default.

~11.4.0

>=11.4.0 <11.5.0. Patch releases only.

Core packages when you must stay on a minor for a while (for example because a contrib module is not ready for the next minor).

^3.0

>=3.0.0 <4.0.0.

Most contributed modules. A new major branch is never installed by accident.

^1.0 || ^2.0

Either major branch.

Modules in transition between two majors. Composer prefers the highest version that satisfies every requirement.

3.1.7 (exact)

Exactly that version.

Emergency pins. Always leave a comment in the commit message explaining when the pin can go.

Rules that save a lot of grief:

  • Quote the pattern in composer update "drupal/*"; the shell otherwise expands it, and zsh and fish fail on the unquoted form. On Windows, double quotes are safer than single quotes.

  • A plain composer update does not cross a major version. To move a module from 1.x to 2.x you must say so with composer require drupal/example:^2.0 --update-with-all-dependencies, after reading the project page and its issue queue.

  • Use --dry-run first when many packages move; it prints what would change without touching files.

  • composer outdated lists what could move. The Drupal guide colours its output: yellow means the new version is inside your constraint and composer update takes it, red means a constraint or a major version blocks it.

  • --with-all-dependencies (or -W) lets Composer also update the dependencies of the named package, which is almost always needed for core because drupal/core-recommended pins exact library versions.

Updating core with Composer

The official procedure for minor and patch updates (for example 11.4.10 to 11.4.11, or 11.3 to 11.4) follows. Major upgrades are covered in Upgrading from Drupal 10 to 11 and Preparing for Drupal 12.

# 1. What is outdated? No line starting with drupal/core means Composer sees no update.
composer outdated "drupal/*"

# 2. Read Reports > Status report first and fix every error and warning.

# 3. Back up the database (and code and files, see below).
ddev drush sql:dump --result-file=../backups/meridian-before-update.sql

# 4. Confirm the project uses core-recommended and check Drush's version.
composer show drupal/core-recommended
composer show drush/drush

# 5. Preview, then update core together with everything it pins.
composer update "drupal/core-*" --with-all-dependencies --dry-run
composer update "drupal/core-*" --with-all-dependencies

# 6. Let Drupal catch up and rebuild caches.
ddev drush updatedb
ddev drush cache:rebuild

# 7. If you use configuration management, export after the update, because core updates
#    can change config. Look at the diff first.
ddev drush config:export --diff
ddev drush config:export

Notes on that procedure:

  • If the project uses only drupal/core (no core-recommended), the command is composer update drupal/core --with-dependencies.

  • The official guide enables maintenance mode around the update with drush state:set system.maintenance_mode 1 and turns it off with drush state:set system.maintenance_mode 0, each followed by drush cache:rebuild. drush updatedb switches maintenance mode on automatically while it runs.

  • Read the core release notes before updating a minor: contributed modules and themes may need new releases, while patch releases normally do not.

  • Test as an anonymous visitor as well as a logged-in user; a broken anonymous page cache is a common surprise.

  • On production, commit composer.json and composer.lock, and run composer install --no-dev followed by drush updatedb. Do not run composer update on a server.

Staying on a core minor

Previous minors receive only security fixes, and a contributed module that is not yet compatible with a new minor may force you to wait. To stay inside the 11.3 line for the moment, require every core package with a tilde constraint:

composer require drupal/core-recommended:~11.3.0 drupal/core-composer-scaffold:~11.3.0 drupal/core-project-message:~11.3.0 --update-with-all-dependencies

Include every drupal/core-* package that appears in composer.json; a forgotten one keeps pulling a different version. To unpin and follow minors again:

composer require drupal/core-recommended:^11.4 drupal/core-composer-scaffold:^11.4 drupal/core-project-message:^11.4 --update-with-all-dependencies

Be aware of the support window: on 2026-10-30 security releases cover 11.4.x, 11.3.x and 10.6.x, and 11.3.x and 10.6.x lose security support when 11.5.0 and 12.0.0 are released (planned for the week of 2026-12-07). Pinning an older minor is therefore a short-term measure, not a strategy.

When core will not update

A dependency constraint elsewhere in the project is the usual cause. Ask Composer who blocks the update:

composer prohibits drupal/core 11.4.11
composer why-not drupal/core 11.4.11     # alias of prohibits
composer update drupal/core "symfony/*" --with-dependencies

Other situations the official guide lists:

  • A constraint blocks a security fix in a library. The guide’s example is Twig: you can temporarily override the constraint with an inline alias of the form composer require "twig/twig:3.26.0 as 3.22.2" (the version numbers are only an example). This is an exact-version workaround, composer validate warns about it, and it must be removed as soon as drupal/core-recommended allows the newer version.

  • Composer reports the update but the Status report shows the old core. Remove the core directory and reinstall: rm -rf web/core/ then composer install, then clear caches.

  • Something fails midway. Stop, search the project queues, and restore from the backup rather than improvising.

Updating modules and themes

Contributed projects are updated the same way, one at a time or all together:

# What is outdated, and does any installed package have a known security advisory?
composer outdated "drupal/*"
composer audit

# Update every Drupal package, or a single module, inside the existing constraints.
composer update "drupal/*" --with-all-dependencies
composer update drupal/pathauto --with-all-dependencies

# Cross a major version deliberately (read the project page and issue queue first).
composer require drupal/example_module:^2.0 --update-with-all-dependencies

# Finish: database updates, caches, and the configuration the update may have changed.
ddev drush updatedb
ddev drush cache:rebuild
ddev drush config:export --diff

The same guide recommends Composer 2.x and Drush 13 or later, asks for a database backup before every update, and suggests committing the updated composer.lock and checking the interface for "Broken/Missing" handlers after drush updatedb. Removing a module is the opposite order: uninstall it on the server first (drush pm:uninstall name or /admin/modules/uninstall), deploy, and only then composer remove drupal/name. Removing the code first leaves Drupal with an installed module whose files are missing, which later blocks updatedb.

Drush no longer manages code. If an old runbook still uses a Drush command from the left column, replace it:

Old Drush command Use now

drush pm-updatestatus

composer outdated "drupal/*"

drush pm-updatecode

composer update drupal/name

drush pm-update

composer update followed by drush updatedb (both steps are needed)

drush dl

composer require drupal/name

drush pm:security

composer audit

Before an update, look at the project page of each module that changes: the release notes name new requirements, removed features and the update path. For a major jump, check whether the module still supports your core version by reading its info.yml (core_version_requirement) as explained in the versions page.

Removed core modules are updated by installing their contrib versions

Core removes modules in each major. Color, Quick Edit, RDF, Aggregator and HAL went in 10.0; Actions UI, Activity Tracker, Book, Forum, Statistics and Tour went in 11.0; and Drupal 12 moves Ban, Contact, Field Layout, History, Search, Settings Tray, Shortcut, Telephone and Toolbar to contrib while removing Claro, Olivero and Stable 9. The update rule is the same every time: before the core update that removes a module you use, install the contrib version of it with Composer, and do not uninstall the core module first. Details and replacements are in Removed Core Modules and Replacements. A site that updates core first discovers that drush updatedb stops on a module that is in core.extension but missing from the codebase.

Applying patches with cweagans/composer-patches

A fix often exists in an issue queue long before it ships in a release. The community standard for applying it in a Composer project is the cweagans/composer-patches plugin. Version 2 is current, and the Drupal guide installs it with composer require cweagans/composer-patches:~2.0 --update-with-dependencies.

Principles:

  • Store patches in your repository (for example in patches/) and reference them by local path. Patches downloaded at install time from a remote URL are a supply-chain risk and break when the remote file changes. The plugin’s own documentation also warns against patches generated from merge-request URLs, because their content changes when the merge request gets more commits.

  • Every patch is documented with the ticket it comes from, so that it can be dropped when the fix ships.

  • Patches are listed in composer.json (or in a separate patches file) and recorded in patches.lock.json, which is committed.

The expanded patch format

Version 2 accepts the compact format of version 1 (description mapped to URL) and an expanded format. Only description and url are required; sha256, depth and extra are optional:

{
    "extra": {
        "composer-patches": {
            "package-depths": {
                "drupal/core": 2
            }
        },
        "patches": {
            "drupal/core": [
                {
                    "description": "Issue 0000000: example core fix (placeholder issue number)",
                    "url": "patches/core/example-core-fix.patch",
                    "sha256": "REPLACE_WITH_SHA256_OF_THE_PATCH_FILE",
                    "depth": 2
                }
            ],
            "drupal/pathauto": [
                {
                    "description": "Issue 0000000: example contrib fix (placeholder issue number)",
                    "url": "patches/contrib/pathauto-example-fix.patch"
                }
            ]
        }
    }
}

How it works and what to remember:

  • Depth. The depth is the -p value passed to patch. The default is 1 (default-patch-depth). A patch made from the Drupal core repository contains paths such as a/core/lib/..., while the plugin applies it inside the installed package directory web/core, so core patches need depth 2 in a project whose web root is web/; contrib patches normally use depth 1. A per-patch depth wins over package-depths, which wins over default-patch-depth. The Drupal guide gives the same advice for the older patchLevel key of version 1.

  • Other settings live under extra.composer-patches: patches-file (default patches.json, for a separate file with a root-level patches key), ignore-dependency-patches, disable-resolvers, disable-downloaders and disable-patchers.

  • patches.lock.json. The first composer install with the plugin enabled creates it. It holds a hash and the fully expanded definition of every patch, and when it exists, installs use it instead of re-resolving composer.json. Commit it. A missing sha256 is calculated and written into the lock file.

  • Duplicates. Patches with the same URL or the same hash are considered identical; the first one added wins and later duplicates are ignored silently.

  • Dependency patches. A dependency’s root composer.json can declare patches that you inherit; use ignore-dependency-patches if you do not want some of them.

Commands

After you add, remove or change a patch definition, rebuild the lock file and re-apply:

composer patches-relock     # alias: composer prl. Re-discover patches, write patches.lock.json
composer patches-repatch    # alias: composer prp. Delete and reinstall patched packages
composer patches-doctor     # alias: composer pd. Checks for common problems; attach its output to bug reports
composer install            # applies the locked patches on any machine

Patches and updates

  • A patch that no longer applies after an update makes composer update or composer install fail, which is exactly what you want: a silent skip would deploy unpatched code. In version 1 this was controlled by "composer-exit-on-patch-failure": true in extra; the version 2 configuration reference lists no such key, so check the plugin’s documentation for the behaviour of the version you installed and test it once by corrupting a patch in a throwaway branch.

  • After every update look at whether the ticket behind a patch was fixed in the new release, and then remove the patch. Keeping an applied-upstream patch causes a failure on the next update.

  • The Drupal guide notes that composer.lock may not reflect patch information after a composer install; running composer update --lock regenerates the lock hash from composer.json.

  • Security-related patches (for example one that backports a fix from an unreleased advisory) are not to be invented by the site owner: the Security Team publishes the fix with the advisory and the release, and that release is what you should deploy.

Database updates: drush updatedb, update.php and drush deploy

New code usually carries new update functions. Core and modules declare them in hook_update_N() (schema or data changes) and hook_post_update_NAME() (changes that need the full system available, for example entity saves). Both must remain procedural functions in the .install and .post_update.php files even though most hooks became object-oriented methods in 11.1; see The Update API and Post-Updates for how the Meridian module writes them. Drupal records which updates ran in the key/value store, so running the step twice is harmless.

The step is mandatory after any code update, even a patch release. Run it in one of three ways:

ddev drush updatedb                # alias: updb. Applies pending updates. Enables maintenance mode while it runs.
ddev drush updatedb --no-cache-clear   # skip the final cache clear when another command clears caches next
ddev drush updatedb:status         # Lists the pending updates without running them
ddev drush cache:rebuild           # alias: cr

Source: Drush: updatedb

  • update.php. Visiting /update.php in the browser does the same work for a user with the "Administer software updates" permission. Access without logging in is controlled by $settings['update_free_access'] in settings.php, which must stay FALSE (the default) on every real site, because a TRUE value lets anyone run the updater.

  • drush deploy. The deployment command wraps the sequence that a release needs. The Drush topic documents it as running, in order, drush updatedb, drush config:import, drush cache:rebuild, drush deploy:hook and, on Drupal 11.2 and later, drush cache:warm. The fourth step runs hook_deploy_NAME() functions, a Drush feature (not Drupal’s) for changes that need the imported configuration to be present, as opposed to hook_post_update_NAME(), which runs before the configuration import.

# The whole release step on staging or production, after `composer install --no-dev`:
vendor/bin/drush deploy -v -y

# The same thing spelled out:
vendor/bin/drush updatedb --no-cache-clear -y
vendor/bin/drush config:import -y
vendor/bin/drush cache:rebuild
vendor/bin/drush deploy:hook -y

The configuration import in a deployment is the other half of a feature or an update that changes configuration. If you update locally, export the changed configuration, and commit it together with composer.lock; if you do not, the next config:import on the server reverts the configuration the update changed. This is the single most common cause of "the update worked locally but the feature disappeared in production". Configuration management itself is covered in Configuration Management, and the surrounding pipeline in Deployment and CI/CD.

When updatedb fails

The official troubleshooting guide traces most failures to a module or theme that is listed in core.extension but whose code is missing or incompatible:

  1. Read the exact message from drush updatedb or update.php; it names the extension.

  2. Check the code: is the folder missing or damaged under web/modules or web/themes? Restore the right version with Composer.

  3. Check compatibility: does the extension’s info.yml declare core_version_requirement that includes your core, and a php requirement that your PHP satisfies?

  4. If the code cannot be found and is no longer wanted, remove the entry from core.extension (for example with drush config:edit core.extension), use a "missing module" fixer, write a hook_update_N() that removes it, or add a dummy module with the same machine name and uninstall it. Clean leftover rows in the config, key_value, locale and cache tables.

  5. Clear caches and run the update again.

Always back up the database and the code before attempting any of these.

Backups before every update

An update is the moment a site is most likely to break, so a restorable backup is part of the update, not an extra. The Drupal guide lists what to keep: a full database dump, the user-uploaded files (usually sites/default/files, plus any private files directory), a snapshot of the codebase (a Git tag or commit counts), and a documented, tested rollback plan. Tools are the hosting provider’s own backups first, Backup and Migrate where there is no shell, Drush (drush archive:dump creates one file with database, files and code, drush archive:restore restores it, drush sql:dump writes the database only), mysqldump or pg_dump with tar, and ddev snapshot locally.

ddev snapshot --name before-core-update      # local database snapshot
ddev drush archive:dump                       # code, files and database in one archive
ddev drush sql:dump --gzip --result-file=../backups/meridian-$(date +%F).sql
ddev snapshot restore before-core-update      # if the update must be rolled back locally

Strategy, retention and restore tests belong to Backups and Disaster Recovery. Remember that a Composer rollback is easy (git checkout the previous composer.lock and run composer install), but a database rollback after updatedb is only possible from a backup, because update functions are not reversible.

The Update Status report

Since 11.2 the module formerly called Update Manager is Update Status (machine name update). It still provides the Available updates report under Reports > Available updates (/admin/reports/updates). Once a day by default it asks updates.drupal.org for release information about the installed projects and shows, for core and each module or theme, the installed version, the recommended release and whether an update is a security update or merely a bugfix. Entries are coloured: an unsupported release or one with a known security fix stands out.

Its settings are on the report’s Settings tab (/admin/reports/updates/settings) and live in the update.settings configuration object:

# config/sync/update.settings.yml
check:
  disabled_extensions: true
  interval_days: 1
fetch:
  url: ~
  max_attempts: 2
  timeout: 30
notification:
  emails:
    - webmaster@meridian-museum.example
  threshold: security
  • check.interval_days is how often core fetches release data, check.disabled_extensions whether disabled projects are checked too (the shipped default is true).

  • notification.emails is the list of addresses to mail when a project needs an update, and notification.threshold chooses all (every newer release, the default) or security (security updates only). Mail goes out from cron, so cron must run; see Cron, Queues and Maintenance.

  • The report needs outbound HTTPS to updates.drupal.org. On a locked-down production network allow that host or the report will show "Failed to get available update data".

  • Projects installed from a Git repository, a Composer VCS repository or a fork have no release data and appear as unknown; do not read this as "up to date".

The report is a signal, not a substitute for composer audit: it does not see libraries from outside Drupal.org, and it cannot say whether your patched copy contains a fix.

Critical security announcements on the status report

Core also reads a separate feed of highly critical advisories and public service announcements and shows them to users with the "Administer site configuration" permission on the status report and some administration pages. The change record that introduced it dates it to Drupal 9.2, while the current Responding to critical security update advisories guide describes it as starting with 9.3.0. Fetching is controlled by the system.advisories configuration object (shipped as enabled: true and interval_hours: 6), served by the system.sa_fetcher service, and can be turned off in settings.php with $config['system.advisories']['enabled'] = FALSE;, which is not recommended. The feed is JSON at updates.drupal.org/psa.json, with entries that carry title, link, project, type, is_psa, insecure and pubDate. For an announcement tied to a future release, the insecure list is filled in only after the release is published.

If the status report says "Failed to fetch security advisory data", check in order that the feed answers (an empty list, [], means no highly critical advisories at present), that PHP’s OpenSSL is configured or the HTTP fallback is on, that no firewall blocks the web server user (test with curl as that user), and the error log for JSON decoding errors.

Automatic Updates, Package Manager and Project Browser

Composer-only does not mean Composer-by-hand forever. Three pieces are meant to make code updates safer for site owners who do not live in a terminal:

  • Package Manager is a core module (core/modules/package_manager) with experimental lifecycle status and no user interface of its own. It is an API that stages Composer operations in a sandbox copy of the project, validates them with events, and applies the result to the live codebase. It depends on the Update Status module.

  • Automatic Updates (drupal/automatic_updates, contrib) builds the user-facing part on it. The 4.x releases require Drupal 11.2 or later and rely on core’s Package Manager (4.1.0, released in January 2026, merged the extensions submodule into the main module); 3.x supports Drupal 10.1 and 11. The project page marks it as receiving maintenance fixes only.

  • Project Browser (contrib) lets an administrator browse projects and queue installs from the interface, using the same Composer layer. The project page lists no supported stable release at the time of writing, and whether it will enter core is tracked in its issue queue.

What Automatic Updates does, and does not do, matters more than the name suggests:

  • It updates patch-level releases of core (for example 11.4.10 to 11.4.11) first in a sandbox. Minor-version updates are experimental and disabled by default, and major upgrades are not supported.

  • Updates are attended: after the readiness checks pass, an administrator starts the update from a form. Unattended cron updates are not supported; a console script in 3.x can be run from a crontab as a more privileged user, which is scheduled execution rather than unattended updating.

  • Updates of contributed modules and themes are marked experimental and "not ready for production".

  • Multisite is not supported; a development snapshot cannot be updated; an update cannot be undone or downgraded.

  • It needs a Composer-managed site, Composer 2.6 or later, and a codebase that PHP can write to, which is exactly what a hardened production server avoids. The project page warns that it is likely incompatible with hosts such as Acquia and Pantheon, and recommends updating locally and deploying the result.

  • Readiness checks run regularly and flag blockers such as pending database updates, a read-only file system or insufficient disk space.

For the Meridian Museum the policy is: Composer in CI for everything; Package Manager and Automatic Updates are evaluated only for small sites with no CI and a writable codebase. Setup details and a longer discussion are in Automatic Updates and Project Browser.

"Manual" updates in the official guide

The Updating Drupal guide still has a page for updating core "manually" (downloading an archive and replacing files). It is meant for people with no shell access, and it applies only to projects that were not built with Composer. It is not available as an interface feature: the UI that did the file copying was removed. New sites should not be built in a way that needs it.

The security advisory process

Drupal has an organised security team, and understanding its rules tells you what an advisory means, which releases it covers and when to expect one.

Identifiers and types

  • SA-CORE-YYYY-NNN is a security advisory for Drupal core. 2026 examples are SA-CORE-2026-014 (14 October, rated Critical, 19/25, remote code execution) and SA-CORE-2026-015 and -016 (also 14 October, Moderately critical, cross-site scripting).

  • SA-CONTRIB-YYYY-NNN is an advisory for a contributed module, theme or distribution, for example the batch published on 28 October 2026 (SA-CONTRIB-2026-254 to -292). Each advisory lists a CVE identifier.

  • PSA-YYYY-MM-DD is a public service announcement: a heads-up, usually published on the Monday before a Wednesday security release, for issues rated highly critical or critical that could easily be turned into automated attacks. It gives site owners time to prepare for the update.

Risk levels

The team scores each vulnerability from 0 to 25 with the Security Team’s risk calculator, based on the NIST Common Misuse Scoring System. The score maps to a level:

Score Risk level

20-25

Highly critical

15-19

Critical

10-14

Moderately critical

5-9

Less critical

0-4

Not critical

Six metrics add up to the score: access complexity, authentication required, confidentiality impact, integrity impact, whether an exploit exists, and target distribution (how many installations are affected). The scale applies to advisories published after 6 August 2014. A "moderately critical" cross-site scripting issue that needs an authenticated editor is not a fire drill; a critical remote code execution issue is, and the Meridian runbook treats the two differently (see the routine below).

Release windows

Core security releases are scheduled for the third Wednesday of each month, beginning at 12:00 America/New York, and bugfix releases for a window in the first Wednesday period. "Not all core security windows will include a core security release." Rarely, a security release comes outside the window; a PSA precedes it. Contributed-project advisories are published in batches (the October 2026 batches above are an example), so a monthly look at the calendar is not enough for a site with many modules.

flowchart TD M[Monday: PSA for a critical or highly critical issue] --> W[Wednesday: advisory and fixed release published] W --> R{Is the project installed on the site?} R -- no --> X[No action] R -- yes --> V{Is the installed version in the affected range?} V -- no --> X V -- yes --> U[Composer update, test, deploy, drush deploy] U --> C[Confirm with composer audit and the status report]

What the policy covers

Coverage is decided by the security advisory process and permissions policy:

  • Drupal core. The two most recent minor versions receive advisories. In practice, on 2026-10-30 the security windows list 11.4.x, 11.3.x and 10.6.x; 11.2.x and 10.5.x lost support when 11.4.0 was released, and 11.3.x and 10.6.x lose it when 11.5.0 and 12.0.0 are released, planned for the week of 2026-12-07. Drupal 10 ends on 2026-12-09; do not start new sites on it.

  • Contributed projects. Only stable releases in supported major branches of projects that are opted in are covered. Development snapshots, alpha, beta and release-candidate versions are not covered. "Supported" means supported by both core and the maintainer.

  • Opt-in. A project must be a full project (not a sandbox), its maintainer must hold the "vetted" role, it must have no open issues tagged security, and a new project waits ten days. Projects created before March 2017 are covered automatically. A covered project page shows a grey shield and a note saying that stable releases are covered.

  • What is not covered. External libraries or plugins not hosted on Drupal.org (those are the library authors' business; libraries bundled in core, such as jQuery, are covered), vulnerabilities that require advanced permissions (administering users, permissions, modules, configuration, views or fields, or any permission with "restrict access"), sandbox projects, projects in external repositories (some distributions), and pre-release versions. Hard-to-exploit issues may be fixed publicly in the public queue, at the team’s discretion.

  • Secrecy. Problems are kept private until an advisory and a fix are ready, then publicised widely. Report suspected vulnerabilities to the Security Team, never in a public issue.

The practical rules that follow:

  1. A site that runs an unsupported core minor, or an unsupported contrib release, has no advisory coverage at all: a bug may be fixed in the new branch only, silently, and the site’s owner learns nothing.

  2. A site that runs a development snapshot (a dev version, or a Git checkout) cannot be told by Drupal whether a fix is included; the owner must read each advisory and decide.

  3. Covered does not mean safe: an advisory only exists for what the team knows, and third-party libraries and your own custom code (meridian_collection) are outside the Drupal Security Team’s remit.

Subscribing to advisories

An advisory you never see protects nobody. The Meridian operations team uses more than one channel:

Route the feeds to the channel the on-call person already watches, and assign an owner. A mailbox nobody reads is the usual way a critical advisory is missed for a week.

composer audit and the pipeline

Composer’s audit command compares the packages in composer.lock with published security advisories. For Drupal, packages.drupal.org supplies advisories for the projects it hosts, and Packagist supplies those for ordinary PHP libraries (Symfony, Twig, Guzzle and so on). The Drupal guide recommends it as the replacement for drush pm:security because it also checks libraries that are not Drupal projects.

composer audit                          # human-readable report; exit code is non-zero when advisories are found
composer audit --locked                 # audit composer.lock even without vendor/ (useful in CI)
composer audit --format=json > audit.json

Limits to keep in mind:

  • The Drupal advisory data covers published security advisories. A contributed release marked insecure on Drupal.org without an advisory, or an unsupported release, does not appear; third-party tools exist that add those checks to the audit output.

  • An advisory entry means a published issue affects the installed version range, not that your site is exploitable. Read it: it may need a permission or a module configuration you do not use.

  • A new advisory can appear for code you did not change. Run the audit on a schedule as well as on every pull request.

A minimal GitHub Actions job for the Meridian repository:

name: security-audit
on:
  pull_request:
  schedule:
    - cron: "17 6 * * 1-5"   # weekdays, so new advisories are caught the same day

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          tools: composer:v2
      - run: composer install --no-interaction --no-progress --no-dev
      - run: composer audit --locked

Pipeline design is covered in Deployment and CI/CD, and hardening the rest of the site in Security Hardening.

A monthly patch routine for the Meridian Museum

Updating monthly beats updating rarely and in a panic. The routine below is a template; adjust the cadence to the site’s risk.

When What

First week of the month

Bugfix release window. Create a branch, run composer outdated "drupal/*" and composer audit, then composer update "drupal/*" --with-all-dependencies. Read the release notes of core and every module that moved. Run the test suite, drush updatedb and drush config:export --diff in DDEV.

Third Wednesday

Security window. Read the advisories of the day (the Monday PSA warns about the dangerous ones). If any installed project is affected, treat the update as urgent: same branch, same steps, smaller scope.

Same day, after merge

CI builds an artifact from composer.lock; deploy first to staging, smoke-test as anonymous and as the Curator role, then to production with drush deploy.

After deploy

Check Reports > Status report and Recent log messages; confirm that composer audit is clean; delete the temporary patches whose upstream issue was fixed; commit patches.lock.json and configuration.

Quarterly

Review the support calendar: which minor is next, when the current one loses security coverage, which contrib modules lack a release for it. Plan the 12.0 preparation (see Preparing for Drupal 12).

Escalation rules:

  • Highly critical or critical, and the project is installed: update within hours of the release, even outside the routine. Consider a temporary mitigation the advisory describes (disable a module, block a route in the web server) until the deploy.

  • Moderately critical: update within the same week.

  • Less critical or not critical: include in the next routine update.

  • Anything affecting an unsupported or unavailable release: plan a replacement; there will be no fix.

A script that wraps the CI-side steps keeps the routine repeatable:

#!/usr/bin/env bash
# scripts/monthly-update.sh -- run on a branch in DDEV, then open a pull request.
set -euo pipefail

composer outdated "drupal/*" || true
composer audit || true

composer update "drupal/*" --with-all-dependencies
composer patches-relock
composer validate --strict

ddev drush updatedb -y
ddev drush config:export -y
ddev drush cache:rebuild

git status --short composer.json composer.lock patches.lock.json config/sync

Update decision flow

flowchart TD S[Update needed] --> Q{Does the site have a writable codebase and no CI?} Q -- no --> C[Composer on a developer or CI machine] Q -- yes --> A[Automatic Updates for patch-level core, or Composer in a shell] C --> P{Is a fix only available as a patch?} P -- yes --> CP[cweagans/composer-patches with a local patch and patches.lock.json] P -- no --> D[composer update or composer require] CP --> D D --> DB[drush deploy: updatedb, config:import, cache:rebuild] A --> DB

Common errors and pitfalls

  • composer update on production. It changes versions that nobody tested. Use composer install from the committed lock file.

  • Forgetting drush updatedb. The new code runs against the old schema; symptoms range from a status report warning to white screens. Make it part of every deploy.

  • Not committing composer.lock or patches.lock.json. Two environments then build different code.

  • Editing web/core or web/modules/contrib directly. The next Composer run silently reverts it. Use a patch.

  • Remote patch URLs and merge-request patches. They change underneath you; download, review and commit the file.

  • A stale patch. After the upstream fix lands, the patch fails to apply (or double-applies). Drop it when the update carries the fix.

  • Removing code before uninstalling the module. Uninstall on the server first, deploy, then composer remove.

  • Updating core before installing the contrib replacement of a removed module. updatedb stops on a missing extension.

  • Unquoted wildcards (composer update drupal/*), which the shell expands or rejects.

  • Pinned core with ~ left in place for months. The pin keeps the site off the supported minors.

  • Exact-version overrides for a library (twig/twig:X as Y) never removed; composer validate warns about them for a reason.

  • $settings['update_free_access'] = TRUE; left in settings.php after an emergency run of update.php.

  • Reading "no updates" from Update Status as "secure". It does not know about your custom code, bundled libraries from outside Drupal.org or unpublished vulnerabilities.

  • Relying on development snapshots. No advisory coverage, and no way for Drupal to say whether a fix is included.

  • An unmonitored security mailbox. Assign an owner and a backup.

  • Treating "stable on Drupal 10" as a plan. Drupal 10 ends on 2026-12-09; upgrade to 11.3 or later (11.4 or 11.5 recommended) first.

Version notes and corrections of older material

Version Change relevant to this page

10.0

Several modules leave core (Color, Quick Edit, RDF, Aggregator, HAL). Install the contrib version before updating.

10.3

/user/logout is CSRF-protected and the security.enable_super_user parameter appears (see the hardening page).

10.4 and 11.0

"Add new module", "Add new theme" and the install-from-URL page on the updates report are removed.

11.0

Actions UI, Activity Tracker, Book, Forum, Statistics and Tour leave core. IIS support is removed.

11.2

The update-by-UI feature (authorize.php) is removed and deprecated for removal in 12.0.0; the Update module is renamed Update Status. drush deploy gains a final cache:warm step.

11.3

PHPUnit 10 is dropped; HTMX headers must be passed by proxies and CDNs.

11.4

Standard install no longer creates Article and Page; drupal/legacy-project is abandoned; drupal/core-recommended no longer pins Guzzle, Twig or Symfony polyfills; the 11.4.11 .htaccess replaces assert.active with zend.assertions, so site owners who manage that file by hand must edit it.

12.0 (planned)

Refuses to upgrade from 11.2 or earlier (start from 11.3 or later); removes Migrate Drupal and Migrate Drupal UI without a contrib move; the deprecated authorize.php code is removed. Requirements (PHP 8.5, MariaDB 10.11, PostgreSQL 18 or 19) differ slightly between the requirements page and the beta1 notes until the final release settles them.

Corrections of books and old tutorials, in prose: Drupal 10 Development Cookbook (2023) targets Drupal 10.0, PHP 8.1 and Drush 12 and still shows Drush annotations, drush.services.yml and a code-update workflow that predates the removal of the update UI; Drupal 10 Masterclass (2023) presents Recipes, Project Browser and Automatic Updates as future initiatives and shows Drush 12; since 10.4/11.0 and 11.2 the install-from-URL and update-by-UI features are gone, Composer is the only supported way, and Update Status is informational. Neither book covers Drupal CMS, the 11.4 changes, or Drush 13 and 14, where commands use attributes and autowiring and drush.services.yml no longer exists. Older tutorials that say "download the tarball and overwrite core" apply only to non-Composer sites that Drupal no longer supports. See Where Older Books and Tutorials Are Outdated.

Other pages that continue this topic: Drush for the full command reference, settings.php and Environments for per-environment settings, Logging, the Status Report and Troubleshooting for reading results after an update, and Composer and Packages for Composer itself.

Checklist

  • Code changes only through Composer; composer.json, composer.lock and patches.lock.json committed.

  • Servers use composer install --no-dev and drush deploy; composer update never runs on a server.

  • Every patch is a local file with a documented issue, listed in composer.json.

  • Update Status notifications on security, cron running, outbound access to updates.drupal.org allowed.

  • Security newsletter and RSS feeds routed to an owned channel; composer audit runs in CI on a schedule.

  • A tested backup exists before every update; the rollback plan is written down.

  • The site runs a covered core minor (11.4 or 11.3 today) and only stable, covered contributed releases.

  • $settings['update_free_access'] is FALSE.