Drupal’s core is well audited and has a serious security team behind it. The contributed modules that turn core into a real website are a different picture. There are tens of thousands of them, written by volunteers of wildly varying experience, and only a subset are covered by the security advisory policy at all. When you install a contrib module you are trusting code that, in many cases, no security reviewer has ever looked at line by line. On a site that takes payments or holds customer data, that trust is worth checking.
That checking is slow and unglamorous work, and it is exactly the kind of task an LLM is genuinely good at: reading a lot of unfamiliar code quickly and pattern-matching it against a known class of mistake. So I started pointing Claude Code at module code with a narrow brief, not “is this good code” but “where does data that a user controls end up rendered without being sanitised first.”
What I actually ask it to look for
Cross-site scripting in Drupal usually comes from one root cause: a value that a user can influence is printed into a page or an email as raw markup instead of escaped text. Drupal gives you good tools to avoid this. Twig templates autoescape by default, render arrays carry their own safety, and there are helpers like Html::escape() and Xss::filter() for the cases that need them. XSS creeps in at the seams: a string concatenated into #markup, a value marked as safe that should not have been, a template that handles a field as raw HTML, or output that escapes correctly on the web page but is then reused somewhere that does not escape, such as an email body.
So the prompt is concrete. Trace user-controllable inputs. Follow each one to where it is output. At each output point, decide whether it is escaped for that exact context. Flag any path where it is not, and tell me the input, the sink, and why the escaping is missing or wrong. Claude is good at the tracing and the pattern recognition. It is not the judge of whether a finding is real.
The tool finds candidates; a human confirms vulnerabilities. Most of what comes back is not exploitable: the value turns out to be admin-only, or it is escaped one layer up, or the context autoescapes after all. Treating Claude’s output as a list of leads to investigate, rather than a list of bugs, is the whole discipline. Reporting an unverified finding to a security team wastes their time and erodes your credibility. Every report I file, I have reproduced first.
The one that became public
The clearest finding was in Drupal Commerce Core, the e-commerce suite a great many Drupal shops run on. Commerce has an optional checkout step that lets a customer leave a comment with their order. That comment is later included in the order receipt email. The value the customer typed was placed into the receipt without being adequately sanitised, which means a customer could put markup into that field and have it rendered, rather than shown as plain text, when the receipt was generated.
I reproduced it, wrote it up plainly, and reported it to the Drupal Security Team through the proper private channel rather than posting it anywhere public. They confirmed it, a fix was written and released, and a coordinated advisory was published once the fixed version was available. Here is the result.
Commerce Core: Cross-site scripting
- Advisory
- SA-CONTRIB-2026-041
- CVE
- CVE-2026-10769
- Project
- Commerce Core (Drupal Commerce)
- Type
- Cross-site scripting (XSS), via the customer comments checkout pane in order receipt emails
- Severity
- Moderately critical (14/25). Exploitability rated theoretical
- Affected
- Commerce Core 3.3.0 up to, but not including, 3.3.6
- Fixed in
- Commerce Core 3.3.6
- Credited
- Reported by Brian Willows; fixed by the maintainer; coordinated by the Drupal Security Team
Why it is rated low, and why that is the honest read
I want to be straight about the severity, because it would be easy to dress this up as more than it is. The advisory is rated moderately critical, with exploitability marked as theoretical, and there is a good reason for that. The vulnerable feature is the “Comments” checkout pane, and that pane is disabled by default. A site is only affected if it has checkout enabled and has deliberately turned that specific pane on. Many sites never do. So this is a real bug that was worth fixing, not a five-alarm fire, and the scoring reflects that honestly.
That is the right outcome. The point of the security process is not drama, it is getting the fix shipped and the credit recorded accurately. A correctly-scored low-to-moderate finding that is fixed quietly is a success, not an anticlimax.
Why most of them never show
If you only ever see published advisories, you see a fraction of the work, and a skewed one. Most reports never become a public advisory, for entirely legitimate reasons:
- It was not actually a vulnerability. On closer inspection the input was already escaped, or restricted to trusted roles, and the finding does not survive verification. This is the most common outcome and the report is never filed.
- The module is not covered. A large share of contrib has no security-advisory coverage. Issues there are fixed as ordinary public bug reports, with no advisory and no CVE.
- It was a duplicate or already fixed. Someone got there first, or a refactor had quietly resolved it in a later branch.
- It is still embargoed. Anything reported recently stays private until a fix is released. The public advisory, if it comes, can be weeks or months behind the report.
- The fix was folded in quietly. Not every hardening change is judged to warrant a formal advisory.
So a single published advisory sits on top of a stack of verified-but-uncredited fixes, judgement calls, dead ends, and reports still under wraps. That is not a complaint; it is how responsible disclosure is supposed to look. The visible credit is real, but it under-counts the work, and it should.
The honest split
Claude Code did the heavy reading: tracing user-controllable inputs through unfamiliar module code to their output points, and flagging the ones where escaping looked absent or wrong. It is fast and tireless at exactly the part of the job that is tedious for a person.
I did the judgement: deciding which flags were worth chasing, building a working reproduction, scoping the real-world impact, writing the report, and handling disclosure through the proper private channel. The model produced leads. Confirming a vulnerability, and reporting it responsibly, is human work.
This is the same division of labour I describe in my other write-ups, applied to security review rather than building. The AI is leverage on the laborious part. It does not remove the need to know what a real vulnerability looks like, how Drupal’s sanitisation actually behaves, or how to disclose without doing harm. If anything it raises the value of that knowledge, because the bottleneck moves from finding candidates to judging them.
What this means if you run a Drupal site
Every contrib module on your site is code you are trusting, and not all of it has been reviewed by anyone with security in mind. You do not need to audit all of it yourself, but it is worth knowing that the gap exists, keeping modules updated promptly when advisories land, and having someone who can read the code when something looks off. Reviewing the contrib a site depends on, reporting what is wrong through the right channels, and applying the fixes is part of what a proper Drupal support arrangement should cover.
If you would like that kind of attention on a site that handles payments or customer data, that is the work I do. The contact details are below.
