Home | xjm
Image
A long while back, security researcher
Sam Mortenson
reported a cross-site scripting vulnerability in Drupal core's Link module. Essentially, the
options
property on link fields was not being properly sanitized. This meant cross-site scripting was possible under some circumstances -- and, as always for cross-site scripting, we were concerned that the XSS could be combined with other attacks and escalated to more serious exploits.
A fix for this vulnerability was released back in March as
SA-CORE-2025-004
. It's been more than two months now since that advisory was issued, which means that the embargo on disclosures for it is over, and I can tell you a bit more about it.
The conundrum
The vulnerability (known internally as "Anemone") was fairly straightforward, but we faced a conundrum. Over 60 contributed modules extend core's link field formatter, which meant there were two potential problems:
Any of these modules might have had the same vulnerability as core, if they overrode or reimplemented the vulnerable core code.
Any of these modules could break when we changed the Link module's code to mitigate the vulnerability.
Two fronts
Over a long period, we tackled this problem on two fronts:
We researched different potential solutions at various levels of the Link module's API. We looked for a fix that would provide the most complete protection against the vulnerability without breaking downstream code.
Meanwhile, we also audited all the contributed modules hosted on
Drupal.org
. We checked to see which would be fixed by our core fix, which would remain insecure (and therefore require coordinated security advisories), and which might be disrupted by the various proposed solutions.
The best fix
The best fix we came up with was identified by core release manager
longwave
and implemented by framework manager
larowlan
, both members of the Security Team: We would improve core's existing XSS sanitization to properly handle options attributes, and then call that improved sanitization in
LinkFormatter::buildUrl()
and
MenuLinkContent::getUrlObject()
. To achieve this, we needed a new API for better attribute sanitization, and the
Xss::attributes()
method also needed a small improvement to support that new API.
The reason core SAs add duplicated code
We couldn't just make changes to
Xss::attributes()
in a security release, though. Core's XSS utility is used everywhere in core and contrib, and even offered as part of a standalone
utility component
that doesn't depend on Drupal at all. Changing such an important API in a security release would have sweeping consequences, and even making backwards-compatible additions to it would violate
semantic versioning
. Such disruption could introduce regressions for contributed or custom code and thus prevent site owners from updating to the security release -- exactly the opposite of what we wanted.
So, instead of making changes to the XSS utility, we did what (under other circumstances) would be a bad practice: We made a copy of the code for use in the security fix only, and added the changes we needed to that copy.
This is a strategy we often use for core security releases. We duplicate and update code targeting the fix to avoid making disruptive changes during the security window. Then, following the release and (optionally) an embargo period, we file a followup issue in the public core issue queue to refactor our addition into the proper core API. The refactoring happens in one of core's minor releases, when we can safely include API additions, behavior changes, and new deprecations under our
allowed changes policy
Five contributed modules
Once we identified the safest fix, we were able to finalize the list of contributed modules that would still be vulnerable despite our update. In our audit, we found five contributed modules opted into security coverage that did not call
buildUrl()
directly (meaning they also would not call the new sanitization we were adding). Of those:
One did not use the API in a vulnerable way.
One could already be updated in the public issue queue to more properly use
buildUrl()
, without disclosing anything related to the private security issue.
The remaining three needed their own security fixes, and therefore required their own security advisories.
The coordinated release
Drupal practices
coordinated disclosure
: When we know of a vulnerability affecting one or more projects, we publish the details of that vulnerability only after the maintainers are able to fix it. In practice, this means that when core and contrib share the same vulnerability, we publish advisories for all affected projects during the same security window.
Drupal core security windows are on the third Wednesday of every month from 16:00 to 22:00 UTC. We often collect multiple core security advisories in private and publish them in the same release once they're ready, so that site owners don't have to install as many core updates.
However, when we have a complex or potentially disruptive security advisory -- like a core security release that needs to be coordinated with multiple contributed projects -- we try to publish it by itself. If the site owner is prevented from updating immediately because of (say) breaking changes in one of the updated contributed modules, we don't want them to
also
be dealing with other newly disclosed vulnerabilities from other core advisories in the same window. (Plus, every advisory we release in a given window adds complexity for the Security Team and release managers.)
So, on March 19, 2025, we published the following four advisories, all fixing different instances of the same link option attribute sanitization vulnerability:
SA-CORE-2025-004
SA-CONTRIB-2025-024
SA-CONTRIB-2025-025
SA-CONTRIB-2025-026
A team effort
All core security advisories are collaborative efforts by members of the
Drupal Security Team
, but this one was especially involved! Over a dozen people contributed to fixing this security issue over time:
akalata
alexpott
benjifisher
bramdriesen
catch
effulgentsia
jenlampton
larowlan
longwave
mcdruid
pandaski
phenaproxima
samuel.mortenson
, and myself.
Thanks to
Tag1 Consulting
for sponsoring my time on this issue last year through
Patreon
, to
HeroDevs
and
Zoocha
for sponsoring my related Security Team work, and to
Acquia
for funding me and several other contributors to work on it together previously.
I'd also like to give particular acknowledgment to
pandaski
, whose extensive efforts researching the contrib ecosystem were crucial to picking a solution and ultimately getting the advisory on track for release!
Interested in supporting Drupal's security? You can
sponsor me
Tags
Drupal
Drupal Planet
Security
Funding contribution
Drupal core
@xjmdrupal
RSS feed