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.
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, deprecatedcore/authorize.php, theFileTransferandUpdatersystems 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.
Source: Change record: The feature to install a new extension from a URL via the Drupal UI has been removed
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 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-recommendedrequires 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-scaffoldcopies files that live outsideweb/core(index.php,.htaccess,robots.txt,update.php,default.settings.php) intoweb/. A core update can change these scaffold files, so reviewgit diffafter an update, particularly.htaccessandrobots.txt. -
drupal/legacy-projectis 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.lockis committed. Servers runcomposer install, nevercomposer 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 |
|---|---|---|
|
|
Core packages when you want to follow minors. This is what |
|
|
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). |
|
|
Most contributed modules. A new major branch is never installed by accident. |
|
Either major branch. |
Modules in transition between two majors. Composer prefers the highest version that satisfies every requirement. |
|
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 updatedoes not cross a major version. To move a module from 1.x to 2.x you must say so withcomposer require drupal/example:^2.0 --update-with-all-dependencies, after reading the project page and its issue queue. -
Use
--dry-runfirst when many packages move; it prints what would change without touching files. -
composer outdatedlists what could move. The Drupal guide colours its output: yellow means the new version is inside your constraint andcomposer updatetakes 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 becausedrupal/core-recommendedpins 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(nocore-recommended), the command iscomposer update drupal/core --with-dependencies. -
The official guide enables maintenance mode around the update with
drush state:set system.maintenance_mode 1and turns it off withdrush state:set system.maintenance_mode 0, each followed bydrush cache:rebuild.drush updatedbswitches 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.jsonandcomposer.lock, and runcomposer install --no-devfollowed bydrush updatedb. Do not runcomposer updateon 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 validatewarns about it, and it must be removed as soon asdrupal/core-recommendedallows 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/thencomposer 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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
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 inpatches.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
-pvalue passed topatch. The default is 1 (default-patch-depth). A patch made from the Drupal core repository contains paths such asa/core/lib/..., while the plugin applies it inside the installed package directoryweb/core, so core patches need depth 2 in a project whose web root isweb/; contrib patches normally use depth 1. A per-patchdepthwins overpackage-depths, which wins overdefault-patch-depth. The Drupal guide gives the same advice for the olderpatchLevelkey of version 1. -
Other settings live under
extra.composer-patches:patches-file(defaultpatches.json, for a separate file with a root-levelpatcheskey),ignore-dependency-patches,disable-resolvers,disable-downloadersanddisable-patchers. -
patches.lock.json. The firstcomposer installwith 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-resolvingcomposer.json. Commit it. A missingsha256is 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.jsoncan declare patches that you inherit; useignore-dependency-patchesif you do not want some of them.
Source: Composer Patches: Configuration
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
Source: Composer Patches: Commands
Patches and updates
-
A patch that no longer applies after an update makes
composer updateorcomposer installfail, 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": trueinextra; 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.lockmay not reflect patch information after acomposer install; runningcomposer update --lockregenerates the lock hash fromcomposer.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.phpin 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']insettings.php, which must stayFALSE(the default) on every real site, because aTRUEvalue 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:hookand, on Drupal 11.2 and later,drush cache:warm. The fourth step runshook_deploy_NAME()functions, a Drush feature (not Drupal’s) for changes that need the imported configuration to be present, as opposed tohook_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
Source: Drush: Deploy command for Drupal
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.
Source: Deploying a Drupal update
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:
-
Read the exact message from
drush updatedborupdate.php; it names the extension. -
Check the code: is the folder missing or damaged under
web/modulesorweb/themes? Restore the right version with Composer. -
Check compatibility: does the extension’s
info.ymldeclarecore_version_requirementthat includes your core, and aphprequirement that your PHP satisfies? -
If the code cannot be found and is no longer wanted, remove the entry from
core.extension(for example withdrush config:edit core.extension), use a "missing module" fixer, write ahook_update_N()that removes it, or add a dummy module with the same machine name and uninstall it. Clean leftover rows in theconfig,key_value, locale and cache tables. -
Clear caches and run the update again.
Always back up the database and the code before attempting any of these.
Source: Troubleshooting database updates
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
Source: How to back up your Drupal site
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
Source: API: update.settings.yml
-
check.interval_daysis how often core fetches release data,check.disabled_extensionswhether disabled projects are checked too (the shipped default istrue). -
notification.emailsis the list of addresses to mail when a project needs an update, andnotification.thresholdchoosesall(every newer release, the default) orsecurity(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.
Source: Automatic Updates project page
Source: Automatic Updates (Drupal guide)
Source: API: package_manager.info.yml
"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.
Source: Options for updating Drupal core
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).
Source: Security risk levels defined
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.
Source: Drupal core release cycle
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:
-
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.
-
A site that runs a development snapshot (a
devversion, or a Git checkout) cannot be told by Drupal whether a fix is included; the owner must read each advisory and decide. -
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:
-
Email. Log in to drupal.org, open the user profile, and subscribe to the security newsletter on the Edit > My newsletters tab.
-
RSS. All advisories:
https://www.drupal.org/security/all/rss.xml; core:https://www.drupal.org/security/rss.xml; contributed projects:https://www.drupal.org/security/contrib/rss.xml; public service announcements:https://www.drupal.org/security/psa/rss.xml. -
Social. The Security Team publishes as
@drupalsecurityon Mastodon (drupal.community) and Bluesky. -
Inside the site. The Update Status report with
notification.threshold: security, plus the highly critical feed on the status report. -
In the pipeline.
composer auditin CI (next section), which reads published advisories for installed versions.
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.
Source: Drupal security advisories
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
Source: GitHub Docs: Workflow syntax
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 |
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 |
After deploy |
Check Reports > Status report and Recent log messages; confirm that |
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
Common errors and pitfalls
-
composer updateon production. It changes versions that nobody tested. Usecomposer installfrom 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.lockorpatches.lock.json. Two environments then build different code. -
Editing
web/coreorweb/modules/contribdirectly. 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.
updatedbstops 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 validatewarns about them for a reason. -
$settings['update_free_access'] = TRUE;left insettings.phpafter an emergency run ofupdate.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 |
|
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 ( |
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; |
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 |
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.lockandpatches.lock.jsoncommitted. -
Servers use
composer install --no-devanddrush deploy;composer updatenever 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 toupdates.drupal.orgallowed. -
Security newsletter and RSS feeds routed to an owned channel;
composer auditruns 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']isFALSE.