SevyDevy

Guide · Backend

Drupal's October 2026 Field UI RCE Explained: Why Callbacks Need an Allowlist

18 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The Drupal Field UI RCE (SA-CORE-2026-014, CVE-2026-108097) is a critical remote code execution bug, meaning an attacker could run their own code on your server. Drupal fixed it in 11.4.10, 11.3.19 and 10.6.19 on 14 October 2026.
  • The cause is data that names a function. Each row of a Field UI table carried a callback, which is a function named by a string or a class and method pair. Drupal called it without checking it.
  • Drupal normally checks the callbacks it uses while building pages against an allowlist, a fixed list of functions allowed to run. That check guarded the table's outer step, but not the row callback called inside it.
  • The advisory says core alone is not exploitable, but some contributed modules (add-ons from drupal.org) open paths to it, one without any login. Update now, or uninstall Field UI until you can.
  • Beginners can stop after the patching and stopgap steps. Engineers then get the five-line fix, a run of Drupal's own trusted-callback check on harmless inputs, and an undocumented leftover: core still sets the now-ignored callback.

What goes wrong when a web page lets data name a function?

Your server ends up running a function that someone else chose. That is what the Drupal Field UI RCE allowed on sites with certain add-on modules. When a program stores a function's name as data and later calls it, whoever shapes that data decides what runs.

Picture a common setup. A state university runs its department websites on Drupal 11.3, built from the Standard install profile with a dozen add-on modules. The setup is made up, but the bug is real.

Every page on those sites is built from nested PHP arrays. Some of those arrays carry the names of functions to call while the page is being built. In October 2026, Drupal's security team published a fix for one such name that nobody checked.

This guide follows the university's sites through the bug, the check and the patch. The lesson is wider than Drupal. Any language that can call a function by its name has the same trap.

Which words do you need before this bug makes sense?

You need seven short terms. Each one is used again later, with the same meaning.

  • Remote code execution (RCE): an attacker makes your server run code of their choice, with the same access your website has. Drupal rated this one Critical.
  • Callback: a function you hand to other code so that code can call it later. In PHP, a callback can be a plain string such as json_encode, or a class name and method name pair.
  • Render array: Drupal describes every page as a nested PHP array. Keys that start with a hash sign, such as #pre_render, are settings. The other keys are child parts of the page.
  • Pre-render callback: a function Drupal's renderer calls just before it turns an array into HTML. It can rearrange the array, for example to sort table rows.
  • Field UI: the core module, internal name field_ui, behind the Manage fields and Manage display admin pages. Its tables sort each field into a region, such as Content or Disabled.
  • Contributed module, or contrib: an add-on module downloaded from drupal.org, not part of Drupal core.
  • Allowlist: a fixed list of things that are allowed. Anything not on the list is refused.

What is the Drupal Field UI RCE (SA-CORE-2026-014)?

It is a critical flaw in Drupal core's Field UI module that can let an attacker run code on the server. Drupal tracks it as SA-CORE-2026-014, and its CVE number is CVE-2026-108097.

The advisory adds three limits. The flaw is not directly exploitable with core alone. Several contributed modules create serious paths to it, and at least one path needs no login. Custom code may also expose it.

The table shows which releases are affected and where to go. Find your branch first, which is the first two numbers of your version, such as 11.3.

SA-CORE-2026-014 by branch. Version ranges from the advisory, support dates from the 14 October 2026 release notes, as of 28 October 2026.
Your branchAffected versionsFixed inSecurity support
11.4.x11.4.0 to 11.4.911.4.10Until June 2027, when 11.6.0 is released
11.3.x11.3.18 and earlier11.3.19Until December 2026, when 11.5.0 is released
10.6.x10.6.18 and earlier10.6.19Until Drupal 10 reaches end of life in December 2026
12.0.x beta12.0.0-alpha1 to 12.0.0-beta112.0.0-beta2Pre-release (beta)
11.2.x and older 11, 10.5.x and older, 8 and 9Affected from 8.9.0 onwardsNo fixEnd of life: upgrade to a supported branch

For the university, the 11.3 sites need 11.3.19 or newer. Any department still on Drupal 10.5 gets no fix at all. It must first move to 10.6.19 or a Drupal 11 branch.

Who is at risk from SA-CORE-2026-014?

Sites on an affected version with Field UI installed are at risk, and the risk is real when a contributed module or custom code opens a path. Because the advisory names no modules, treat any such site as exposed.

  • Field UI is common. The Standard install profile installs it, and so does the Umami demo profile. The Minimal profile does not.
  • The university's sites used Standard, so every one of them has Field UI unless someone removed it.
  • Sites already on 11.4.10, 11.3.19, 10.6.19 or later releases on those branches are fixed for this advisory.
  • The vulnerable table element ships inside the Field UI module. Sites without Field UI installed do not load it, which is why the advisory offers uninstalling it as a stopgap.

A separate advisory came out the same day. SA-CONTRIB-2026-218 is a critical remote code execution bug in the Entity List contrib module, CVE-2026-108099, fixed in Entity List 3.0.10. Its advisory does not mention Field UI. Updating core does not update Entity List, so check it on its own.

How can data that names a function run code?

PHP can call a function whose name is stored in a variable. The built-in call_user_func_array takes a callback and a list of arguments, then runs it. If that callback came from data, the data chose the function.

Think of an office front desk. Each parcel carries a slip naming the room that should open it, and the clerk carries it there without asking questions. Nothing stops a slip from naming the strongroom.

Map it part by part. The slip is the callback string. The clerk is call_user_func_array. The rooms are every function your PHP code can reach. Where the picture breaks: a human clerk might notice an odd slip, but PHP has no judgement at all.

Here is the pattern in a few lines of plain PHP, not Drupal code. The row names json_encode, a harmless built-in that turns data into text. Pause and predict what the first echo prints before you read the output.

php
<?php
// A row stored as plain data. The callback is just a string.
$row = ['region' => ['#value' => 'content'], '#region_callback' => 'json_encode'];

// Unsafe: run whatever function the data names.
echo call_user_func_array($row['#region_callback'], [$row]), "\n";

// Safer: only run callbacks on a short, fixed list.
function pick_region(array $row): string { return $row['region']['#value']; }
$trusted = ['pick_region'];

if (in_array($row['#region_callback'], $trusted, true)) {
    echo call_user_func_array($row['#region_callback'], [$row]), "\n";
} else {
    echo "Refused: ", $row['#region_callback'], "\n";
}

Run it with php on any PHP 8 install. We ran it on PHP 8.3.6, and it printed two lines. The first was the whole row as JSON text. The second was "Refused: json_encode".

What just happened? The first call ran json_encode, which the table code never meant to call, and returned its output as a region name. The second call checked the name against a list of one and refused it. Replace the harmless name with a dangerous function, and the first call runs that instead.

How do Drupal's trusted callbacks stop this?

Drupal 8.8.0 introduced a rule that #pre_render, #post_render, #lazy_builder and #access_callback callbacks must be trusted. Trusted means a closure, or a method that its class has declared safe.

At first, breaking the rule only logged deprecation warnings. In 11.4.10 the renderer throws an exception instead, as the source and our test below show.

The check lives in a small piece of shared code called DoTrustedCallbackTrait. We loaded the copy from the Drupal 11.4.10 release tag and ran it on PHP 8.3.6 with harmless callbacks. The table shows what it allowed and what it refused.

Drupal 11.4.10's doTrustedCallback, run on harmless test callbacks with the render system's settings, on 28 October 2026.
Callback formHarmless example we ranResult
Plain function namejson_encodeRefused with UntrustedCallbackException
Method listed in the class's trustedCallbacks()Helper::listedRan
Method of the same class, not listedHelper::notListedRefused
Method marked with the TrustedCallback attributeTagged::taggedRan
Method with no attribute and no interfaceTagged::untaggedRefused
Any public method of a class with RenderCallbackInterfaceElementLike::anyPublicRan
Closure, an unnamed function written inlinefn($x) => ...Ran

Look at the row for RenderCallbackInterface. The renderer passes that interface as an extra trusted type. Its source comment says the aim is that all public methods on render elements are considered trusted. Every render element class has it through ElementInterface, including Field UI's table.

That rule is reasonable on its own. Element classes are written by module authors, not by visitors. The gap is in what one of those trusted methods did next.

Why did the trusted-callback check miss the region callback?

The check guarded the outer door, and the unchecked call sat behind it. The renderer checked FieldUiTable::tablePreRender, the table's pre-render method, and passed it because it belongs to a render element.

Inside that trusted method, the old code looped over the table rows. For each row it read #region_callback and ran it with call_user_func_array, to learn which region the row belonged to. No allowlist sat around that inner call.

Where the check ran, and where it did not (before 11.4.10)

Notice that the shield sits on the outer pre-render step only. The row callback two boxes later runs whatever its array says.

Diagram connections:

  • Data that shapes a table to field_ui_table element (Builds rows)
  • field_ui_table element to Renderer trust check (Rendered)
  • Renderer trust check to tablePreRender method (Passes)
  • tablePreRender method to call_user_func_array (For every row)
  • call_user_func_array to Whatever function is named (Runs it)
  • Data that shapes a table: Built by core, contrib or custom code
  • field_ui_table element: Rows, each with a #region_callback
  • Renderer trust check: Checks the #pre_render callback only
  • tablePreRender method: Trusted: a public method of an element
  • call_user_func_array: Runs each row's callback, no check
  • Whatever function is named: Core's getRowRegion, or anything else

In core, Field UI's own forms set that callback to their getRowRegion method, which only looks up the row's region. That is why core alone is safe. The advisory does not say how the contributed modules reach the flaw.

Our reading of the diff is general, not a recipe. The risk exists wherever code lets outside data reach a row's #region_callback in a Field UI table. The same unchecked line is present in the Drupal 8.8.0 and 8.9.0 source.

How do you check your Drupal version and patch it?

Read your version, compare it with the table, then update core with Composer and run database updates. Composer is PHP's package manager, and Drush is Drupal's command-line tool.

  1. Read your current version

    In the admin pages, open Reports, then Status report, and read the Drupal version line. On the command line, drush status --field=drupal-version prints just the version.

  2. Confirm it with Composer

    Run composer show drupal/core in the site's project folder. Read the versions line, which looks like versions : * 11.3.18 on an unpatched 11.3 site.

  3. Compare numbers part by part

    Versions compare one number at a time, not as decimals. So 11.3.9 is older than 11.3.19, because 9 is smaller than 19.

  4. Back up first

    Take a database dump with drush sql:dump, or code, files and database together with drush archive:dump. Store it off the server.

  5. Update core

    Run composer update "drupal/core-*" --with-all-dependencies. It moves core to the newest release your composer.json allows, which may be a newer branch.

  6. Run updates and clear caches

    Run drush updatedb, which puts the site in maintenance mode while it works. Then run drush cache:rebuild.

  7. Check the result

    Read the version again. You want 11.4.10 or newer on 11.4, 11.3.19 or newer on 11.3, or 10.6.19 or newer on 10.6. Then open a Manage display page and drag one field.

The release notes also show a composer require command with an exact version, such as drupal/core-recommended:11.4.10. Drupal's own notes warn that pinning an exact version can make future updates harder. Prefer the update command unless you have a reason to pin.

For the university, one script per server can loop over the department sites. It runs drush status --field=drupal-version before and after, so the team has a dated record of each site's version.

What can you do if you cannot update Drupal today?

Uninstall Field UI until you can update. The advisory says this will provide protection while sites are updated. Your editors lose the Manage fields and Manage display pages until you turn it back on.

  1. See what else would go

    Run drush pm:uninstall --simulate field_ui. Drush uninstalls a module and the modules that depend on it, so read the list first. No core module depends on Field UI in 11.4.10, but a contrib module might.

  2. Uninstall it

    Run drush pm:uninstall field_ui, then drush cache:rebuild. In the admin pages, the same thing lives under Extend, then Uninstall.

  3. Update core when you can

    Follow the patching steps above. Then run drush pm:install field_ui to bring the pages back.

Field UI is only the user interface for the Field module, and it depends only on that module. In the 11.4.10 source it has no uninstall hook, and its only stored setting is field_ui.settings. That setting holds the prefix for new field names, field_ by default, so re-check it if you changed it.

The advisory also says Drupal Steward covers this issue partly. It protects against some known attack paths but not all, so sites should still update.

How did Drupal 11.4.10 fix the Field UI bug?

It stopped calling the row callback at all. The new code reads the row's own region value and checks it against the table's known regions. The same five-line change, one line out and four in, landed in 11.4.10, 11.3.19 and 10.6.19.

diff
--- a/core/modules/field_ui/src/Element/FieldUiTable.php (11.4.9)
+++ b/core/modules/field_ui/src/Element/FieldUiTable.php (11.4.10)
           // Determine the region for the row.
-          $region_name = call_user_func_array($row['#region_callback'], [&$row]);
+          $region_name = $row['region']['#value'] ?? '';
+          if (!isset($regions[$region_name])) {
+            $region_name = isset($regions['hidden']) ? 'hidden' : array_key_first($regions);
+          }

This is the exact change in FieldUiTable.php between the 11.4.9 and 11.4.10 tags. The 11.3 and 10.6 diffs are identical. No other Field UI file changed in these releases.

How a row's region is chosen from 11.4.10 onwards

Every answer ends in a known region name, and no step calls a function named by the row.

Diagram connections:

  • Row reaches tablePreRender to Is the region value known?
  • Is the region value known? to Use that region (Yes)
  • Is the region value known? to Is there a hidden region? (No)
  • Is there a hidden region? to Use hidden (Yes)
  • Is there a hidden region? to Use the first region (No)
  • Row reaches tablePreRender: Its #region_callback is now ignored
  • Is the region value known?: Checked against the table's #regions
  • Use that region: For example content
  • Is there a hidden region?: Display forms have one, titled Disabled
  • Use hidden: Unknown values fall back here
  • Use the first region: Last resort for other tables

Engineers can check their own copy of core with one search. Run grep -rn call_user_func core/modules/field_ui/src from the Drupal root. On 11.4.9 it finds 1 matching line, and on 11.4.10 it finds 0.

What changes for custom code

If custom or contrib code used #region_callback to place rows, that callback no longer runs. Rows now land where their region value says, or in hidden. Core's old callback also rewrote unknown values to hidden inside the row, and the new code does not.

Test any custom Manage display widgets after updating. A row that silently moves to Disabled is the symptom to look for.

An undocumented leftover

In 11.4.10 and 11.4.11, core's EntityDisplayFormBase still sets #region_callback on its rows. The comment above tablePreRender still describes the property. Nothing calls it any more.

Neither the advisory nor the release notes mention this. It is undocumented as of 28 October 2026, so pin your version and do not rely on the property either way.

How do you stop data from naming functions in your own code?

Map allowed names to code you wrote, check any callback you must accept, and search for places where data reaches a dynamic call. The PHP demo above showed the core move: a fixed list beats a free choice.

Map names to functions instead of calling names

If a setting says which behaviour to use, store a short key such as content or hidden. Then pick the function in code with PHP's match expression, a switch that returns a value, or a fixed array. Unknown keys get a safe default, exactly like the 11.4.10 fix.

Check callbacks you cannot avoid

In Drupal, run callbacks through DoTrustedCallbackTrait, and list your safe methods in trustedCallbacks() or mark them with the TrustedCallback attribute. Then check every call made inside a trusted method too, because trust does not flow down on its own.

Search for dynamic calls

Search your custom modules for call_user_func, call_user_func_array and variables used as function names. For each hit, trace where the name comes from. If any part comes from a request, a form value or stored configuration, add an allowlist.

How do you explain the Drupal Field UI RCE in a system design interview?

Treat it as a trust-boundary question: where did data become code, and which check should have stopped it.

  1. Name the bug class

    Say that data chose which function runs. In PHP, a callback stored in an array can name any reachable function.

  2. Show the boundary

    Drupal checks render callbacks against an allowlist. The check covered the table's pre-render method but not the row callback that method ran.

  3. Give the conditions

    Core alone was not exploitable. Contributed modules opened paths, one without login, so impact depends on what is installed.

  4. Describe the fix

    Version 11.4.10 removed the dynamic call. It reads a region value and checks it against known regions, with a safe default.

  5. Cover operations

    Patch to 11.4.10, 11.3.19 or 10.6.19, or uninstall Field UI as a stopgap. Record versions before and after.

  6. Generalise

    Prefer fixed maps over dynamic calls. Where callbacks are needed, allowlist them at every call site, not only the outer one.

Questions people ask

Is my Drupal site hacked if it ran an affected version?
Not necessarily. The advisory says core alone is not directly exploitable, and its score marked exploit status as theoretical on 14 October 2026. Risk depends on which contributed modules and custom code you run. Patch first, then review your logs and installed modules if you are worried.
Does updating Drupal core also fix the Entity List module?
No. Entity List is a contributed module with its own advisory, SA-CONTRIB-2026-218 (CVE-2026-108099), fixed in Entity List 3.0.10. Updating core changes only core packages. Run composer outdated "drupal/*" to see which contrib modules have newer releases, and update Entity List separately if you use it.
Will uninstalling Field UI delete my fields or content?
Not according to the 11.4.10 source, because fields belong to the Field module, not Field UI. In that source, Field UI has no uninstall hook, and its only stored setting is the prefix for new field names. Editors lose the Manage fields and Manage display pages until you install it again. Take a backup before any uninstall anyway.
Why is the bug called remote code execution if core alone is safe?
Because the flaw itself, an unchecked call to whatever function a row names, can run arbitrary code when outside data reaches it. The advisory says core alone does not expose it. Some contributed modules create exploit paths, and at least one needs no login.
Which release should I install if I am on Drupal 11.2?
None on 11.2, because 11.2 is end of life and gets no fix. Upgrade to 11.3.19 or newer, or 11.4.10 or newer. As of 28 October 2026, 11.3 support ends in December 2026, so moving to 11.4 saves you a second upgrade soon.
Why do Drupal render arrays need trusted callbacks at all?
Because a render array mixes page data with keys that make code run, such as #pre_render. If data can set such a key, data picks the code. Since Drupal 8.8.0, those callback keys must name trusted methods or closures. This bug was one callback key outside that rule.

Sources and further reading

Practise this with Lala

Practise