Research

Eliminating Vulnerability Classes at Scale in the Post-Mythos Era

Jeevan JutlaJeevan Jutla9 minute read

Two of the most widely deployed open-source projects in the world disclosed 569 vulnerabilities between them. Fewer than 105 were actually distinct. This is the study of why the same security failures keep returning, and the complete dataset behind it.

Eliminating Vulnerability Classes at Scale in the Post-Mythos Era

This brief is a written version of a talk given at Black Hat 2026. You can watch the recording here.

Most of what a security team calls "work" is not solving new problems. It is solving the same problem over and over again, and the metrics almost everyone relies on are structurally blind to it.

We took every vulnerability n8n and GitLab had publicly disclosed, traced each one back to the commit that fixed it, and grouped findings that a single fix in one place would have prevented. Viewed that way, most of what both projects shipped as "new" turns out to be a repeat of something they had already patched elsewhere.

  • 67% of n8n's disclosures and 58% of GitLab's were repeat issues. The same underlying weakness, fixed again in a different file, kept showing up.
  • 135 n8n advisories trace down to 32 root causes. 434 GitLab CVEs trace to 73. What looked like an unmanageable backlog was really a short list of design decisions.
  • The repeats alone cost roughly $44k at n8n and over $1M at GitLab in bounties, plus 1,117 backports reapplying fixes that already existed.
  • One GitLab vulnerability class ran for 728 days: 20 findings, 19 engineers, each independently writing the same one-line check, none knowing the last had already solved it.
  • None of this shows up on a dashboard. MTTR and fix rate measure how fast findings close. A class fixed five times still scores as five clean fixes.

Recurrence rate

n8n67%

90 of 135 findings were repeats · Apr 2025–Jul 2026

GitLab58%

251 of 434 findings were repeats · Jan 2024–Jul 2026

Findings → root-cause classes

n8n13532

135 findings compress to 32 root-cause classes.

GitLab43473

434 findings compress to 73 root-cause classes.

Why now

For most of application security's history, the field rested on one assumption: finding a vulnerability was hard, and turning it into a working exploit was even harder. Risk was a function of how much time and effort an attacker was willing to spend, not of how many unresolved issues were sitting in your backlog. A backlog of mediums was a backlog of things that would never get weaponized, so deferring them was rational.

That assumption has quietly stopped holding. In a UC Berkeley benchmark of nearly 900 real vulnerabilities, frontier models produced over 150 working exploits, each one for a few dollars and a few minutes of compute. What used to require a strong security researcher is now something you can rent by the token. And the shift is already visible in breach data, not just benchmarks: for the first time in roughly two decades, stolen credentials are no longer the leading way attackers get in. Straightforward vulnerability exploitation has overtaken them, climbing from around 20 percent to 31 percent of confirmed breaches in a single year.

Every patch is now also an exploit blueprint. The same tools that find a bug can read your fix and recover the flaw it was hiding. So each time you patch one instance of a class, you're handing an attacker a labeled example of exactly the mistake to go looking for elsewhere in your code. Fixing a class one instance at a time no longer buys you the protection it used to.

Why prioritization stopped working

No team has ever fixed everything, and that isn't a failure of any particular team; it's the scale of application security. So the industry built frameworks to safely decide what to ignore. CVSS, EPSS, and SSVC all do fundamentally the same job: they score a vulnerability on its own merits and tell you where it belongs in the queue. That worked when attackers were slow, because weaponizing a bug took months and almost nobody could do it.

Microsoft SharePoint illustrates what that bet costs today. In July 2025, two vulnerabilities were disclosed: an authentication bypass and an arbitrary file write. Scored individually, one landed as medium severity and the other as low — precisely the tiers a prioritized backlog is designed to defer. Chained together, they gave an unauthenticated attacker remote code execution on the server, and that is exactly how they were used in the wild. Almost a year later, the same product saw fresh variants of the same bugs under active exploitation, reusing the same persistence technique.

Both halves of that story matter, because they fail in different places. The scoring failed because severity assessed one finding at a time cannot see a chain. The remediation failed because each round of patches closed the specific bugs that were reported while leaving the design decision that kept generating them untouched, which is why the variants came back a year later. When a model can find and chain vulnerabilities for a few dollars, the interesting question stops being which handful of vulnerabilities we fix and becomes whether the class still exists once we've fixed the ones we can see.

The blind spot in the metrics

"Fix everything" sounds like a fantasy target, and it is a fantasy as long as the unit of work stays at the individual ticket. It sits there because of how we measure the work.

Nearly every security team tracks mean time to remediate and fix rate. Both are a real improvement over simply counting open findings, but they share one blind spot: they measure activity, not whether risk is actually falling. And the failure mode is easy to miss precisely because it looks like success. You fix a vulnerability quickly this quarter, and your MTTR looks great. Months later, the same weakness resurfaces in another service, in a slightly different shape; your tooling files it as a brand-new finding with a fresh ticket and a fresh clock, and you fix it quickly again. The better your team gets at this rhythm, the healthier the dashboard looks, while the exposure underneath never moves. It's Goodhart's law applied to security work: the moment a measure becomes the target, it stops measuring the thing you cared about.

Recurrence rate

Recurrence rate answers the question the dashboard cannot: how much of what you've found this period is just a variant of a class you've already fixed? A high rate means the team is spending most of its effort solving the same problems again.

The test for whether two findings belong to the same class is deliberately strict: one fix, in one place, would have prevented both. That is a much sharper boundary than grouping by a shared CWE label. Two findings both tagged "XSS" can have entirely different root causes and belong to different classes; an XSS and a path-traversal bug can belong to the same class if both trace back to the same missing control. The strictness is the point. Anyone can eyeball two findings and argue the grouping either way, and it's the single-fix test that turns this from an opinion into a measurement.

A second number follows naturally from the first. Classes eliminated counts the root causes you've closed for good, and unlike most security metrics, that number only moves downward and stays there. None of this replaces MTTR; the two sit side-by-side and ask different questions. MTTR asks how fast you closed the ticket. Recurrence rate asks whether closing it actually mattered. Fast MTTR alongside high recurrence rate is not a healthy program; it's a team closing tickets briskly while the underlying risk remains.

The evidence and the cost

We tested the idea against two projects with long, fully public security histories. Between April 2025 and July 2026, n8n disclosed 135 GitHub security advisories; between January 2024 and July 2026, GitLab disclosed 434 CVEs. Of those, 67% of n8n's disclosures and 58% of GitLab's were variants of root causes exposed by earlier findings. Every finding was traced to a public advisory and commit and verified against the repository. If a finding couldn't be assigned to a class with confidence, we excluded it. These figures are therefore lower bounds, not estimates.

Metricn8nGitLab
Corpus135 GitHub advisories434 CVEs
Window analysedApr 2025 – Jul 2026Jan 2024 – Jul 2026
Recurrence rate67% (90 of 135)58% (251 of 434)
Root-cause classes3273
Median recurrence half-life49 days105 days
Recurrence tax (variants only)~$44,000~$1.0M
Backports (shipping the same fix again)213904

Grouped by root cause, the duplication is impossible to miss. n8n's 135 advisories reduce to 32 root causes; GitLab's 434 CVEs reduce to 73. That collapse is also what makes "fix everything" tractable. The raw counts describe the scale of the problem; the root-cause counts describe the problems that actually remain. No team can sustainably keep fixing 135 findings forever, but it can genuinely work through 32 design decisions.

The repeats carry a bill. n8n has paid out roughly $66,000 in bounties and GitLab around $1.7 million; at each project's own rates, variants of previously exposed classes account for about $44,000 and $1 million of that, respectively. On top sits the engineering cost: 213 backports at n8n and 904 at GitLab, each one requiring an engineer to reapply a fix for a problem the project already understood.

At n8n, a single prototype-pollution class produced nine findings in about four months, roughly one every fortnight. A safe function that operated correctly already existed in the codebase; none of the vulnerable paths called it. The fix was never a mystery. Nothing made it mandatory, and that is how recurrence survives even when the right control is sitting right there.

When GitLab returns a list, it checks whether the user can access that list, but not whether you're allowed to see each object inside it. The fix is a single line: you confirm the current user can read the object before returning it. But that line had no shared home, so every engineer writing a new endpoint had to remember it from scratch. Its absence produced 20 findings over 728 days, fixed across 77 commits by nineteen different engineers, each writing the same check in isolation, none aware the last one had already solved it. Most were rated medium severity — the tier a prioritized backlog is built to defer, and the tier SharePoint just showed chaining into a critical.

Underneath both failures is the same mechanic. In each project, roughly half of all fixes were applied where the reported vulnerability appeared rather than at a shared point every code path has to cross, which leaves sibling vulnerabilities open by design. That gap is exactly what recurrence rate makes visible, and it's why the classes kept coming back. GitLab runs the more mature of the two programs, and the data reflects it: a lower recurrence rate, classes that resurface more slowly. But the two-year class above emerged out of that mature program. Maturity slows the treadmill. It doesn't get you off it.

What changes

Getting off the treadmill comes down to four shifts, none of which require a new budget line.

1. The unit of work moves from the ticket to the class. You're no longer working a bug; you're working the family it came from, and the job isn't done when the reported instance closes.

2. The fix moves from the reported line to the design decision. Instead of patching wherever the researcher happened to look, you trace the group back to the choice that allowed all of it and fix it once, at a point every code path must cross. In this study, about half of all fixes landed on the reported line instead, which is precisely why the siblings stayed open.

3. Grouping moves from the CWE label to the root cause. Labels describe the symptom. The only test that matters is whether one fix, in one place, would have prevented both findings.

4. The fix moves from a one-time change to a rule enforced on every commit. This shift does the actual elimination, because the next engineer who reintroduces the class gets stopped at the door instead of getting caught by a researcher eighteen months later. Across the two projects, that meant 23 rules for n8n and 65 for GitLab. This is what "classes eliminated" looks like as a number rather than a slogan.

The best teams already do a version of this without naming it: pick a class, rally the engineers behind it, put a ninety-day clock on it, burn it down, and add a check so it cannot return. The method isn't the missing piece. What's missing is a measurement that tells you whether it worked.

About the data

For each disclosure we pinned the fix commit and verified it against the repository. We diffed the code before and after the patch, then traced the path from the attacker-controlled input to the dangerous operation. A finding joined a class only if one fix, at a single shared point in that path, would have prevented every member. Uncertain findings were dropped rather than counted, so every figure is a lower bound.

The n8n identifiers, titles, and commits come directly from n8n's public GitHub Security Advisories. The GitLab class assignments and CVE-to-commit mappings are derived from public CVEs and public commits and should be treated as independent research, not as an official GitLab record. Nine GitLab CVEs have no public fix commit and are marked accordingly; some GitLab CVE pages disclosed in 2026 may not yet be fully populated on the National Vulnerability Database.

The complete dataset — all 569 disclosures, each assigned to a single root-cause class with its root cause, prevention rule, advisory link, and fix commit — is explorable below and downloadable as Markdown. Every identifier and fix commit resolves to its public source.

About Gecko

The hard part was never knowing the fix. It was doing this at the scale a real codebase demands: tracing a group of findings to one design decision, finding every variant, and writing the rule that holds.

Gecko was built to group findings by root cause rather than by label, trace each group to the decision underneath, hunt the variants still open across the codebase, and write the check that stops the class returning on the next commit. None of it depends on the tool — the method works with a spreadsheet and a disciplined team, and the data here is public and reproducible. What changed is the economics. When finding and weaponising a bug costs a few dollars and a few minutes, fixing each instance by hand is no longer affordable, and closing the class is the only thing that scales.

See which classes are still open in your code. Talk to Gecko.

Appendix: the complete dataset

All 569 disclosures across 105 root-cause classes. Search the design decisions, disclosures, and fix commits, and open a class to see its prevention rule and every finding assigned to it.

Download source
105 classes · 569 findingsData: public advisories & fix commits
n1
n8n14 findings

Workflow-authored content rendered on n8n's own trusted origin without a universal neutralization boundary

Root cause

Webhook responses, binary-data responses, and node-generated pages (Form/Chat Trigger) serve author-controlled bytes from the app's own origin, and the anti-XSS defenses (sandbox CSP + output sanitization/encoding) were bolted on per-codepath instead of enforced once at the response boundary, so every new/divergent render path (legacy webhook path, binary path, per-node template, content-type edge case) re-opened the class.

Prevent recurrence

No code path may write an HTTP response body containing author/user-controlled HTML from n8n's origin, nor render such HTML in a same-origin iframe, unless the opaque sandbox CSP (or iframe sandbox attribute) is applied by a shared boundary the path cannot bypass.

DateFindingSeverityAreaFix
2025-04-22GHSA-c8hm-hr8h-5xjwMediumStored XSS through Attachments View Endpoint11a36b75
2025-06-18GHSA-hfmv-hhh3-43f2HighStored XSS in n8n Form Trigger allows Account Takeover via injected iframe and video/source759e5559
2025-08-11GHSA-mvh4-2cm2-6hpgMediumStored XSS in n8n LangChain Chat Trigger Node via initialMessages Parameterd4ef191b
2025-09-29GHSA-58jc-rcg5-95f3HighPossible Stored XSS in “Respond to Webhook” Node May Execute Outside Sandbox68e00955
2025-12-04GHSA-825q-w924-xhgxHighImproper CSP Enforcement in Webhook Responses May Allow Stored XSS553b2445
2026-02-25GHSA-2p9h-rqjw-gm92HighStored XSS via Various Nodes562d8674
2026-03-06GHSA-w673-8fjw-457cMediumXSS and Open Redirect in Form Nodef1a074f9
2026-03-09GHSA-q4fm-pjq6-m63gMediumStored XSS in Form Trigger6f2f384f
2026-03-25GHSA-3c7f-5hgj-h279MediumXSS in Chat Trigger Node via Custom CSS2d9a2ec7
2026-03-25GHSA-qfc3-hm4j-7q77MediumXSS via Binary Data Inline HTML Rendering2d9a2ec7
2026-06-02GHSA-h86q-fx34-gfjrMediumReflected XSS via Facebook, WhatsApp, and Microsoft Teams Trigger Webhook Verification Endpoints16728b30
2026-06-10GHSA-42h7-m79w-wvg5HighStored XSS in Chat Trigger Node71262655
2026-06-10GHSA-v733-mwr6-fgcmHighSame-Origin XSS in Respond to Webhook Node71262655
2026-07-08GHSA-p3rg-hrf9-w9gjHighDOM-Based XSS via Unsandboxed iframe srcdoc in HTML Preview46c1a0e8
n2
n8n10 findings

SQL built by concatenating structural/unbound tokens (identifiers, operators, ORDER BY, LIMIT, casts) with no shared safe-construction primitive

Root cause

DB integration nodes and n8n's own data-table/source-control DDL build SQL by string-interpolating structural fields (table/column/update-key names, WHERE operator, sort direction, LIMIT, cast type) and expression-resolved values, parameterizing only data values, because there is no mandated identifier-escaping / value-binding / allowlist primitive shared across every SQL-emitting site.

Prevent recurrence

No untrusted or expression-derived value may reach a SQL/DDL string except through parameter binding (data) or the shared identifier-escaping/allowlist primitive (identifiers, operators, limits); a raw ${...} interpolation into SQL fails review.

DateFindingSeverityAreaFix
2026-01-12GHSA-f3f2-mcxc-pwjxMediumSQL Injection in MySQL, PostgreSQL, and Microsoft SQL nodesf73fae6f
2026-03-19GHSA-98c2-4cr3-4jc3HighSQL Injection in Data Table Node via orderByColumn Expression2d9a2ec7
2026-04-22GHSA-hp3c-vfpm-q4f7MediumSQL Injection in Snowflake and MySQL Nodes85b77964
2026-04-22GHSA-mp4j-h6gh-f6mpMediumSQL Injection in SeaTable Node85b77964
2026-04-22GHSA-r6jc-mpqw-m755MediumSQL Injection in Oracle Database Node via Limit Field85b77964
2026-05-13GHSA-mhrx-qhrj-673wHighSource Control Pull SQL Injection293d5afd
2026-06-10GHSA-c37g-w77q-m4vpMediumSQL Injection in Postgres v1/TimescaleDB Nodes71262655
2026-06-24GHSA-hwmj-qg4v-cvg9MediumMySQL v1 Node executeQuery Operation Allows SQL Injection via Unparameterized Expression Interpolationa4bc50f9
2026-07-22GHSA-652q-gvq3-74qvMediumSnowflake Node executeQuery Operation Allows SQL Injection via Unparameterized Expression Interpolation2222fe3a
2026-07-22GHSA-jqwr-vx3p-r266MediumPostgresTrigger Node SQL Injection Allows Authenticated Users to Execute Arbitrary SQL on Connected PostgreSQL Instances46bb1120
n3
n8n9 findings

No prototype-safe object-construction primitive; untrusted keys written into prototype-inheriting objects via dynamic bracket assignment

Root cause

Across parsers, caches, accumulators and clone/merge helpers, code builds plain `{}` objects and writes attacker-controlled key names into them with dynamic bracket assignment (or lodash set / xml2js name mapping), with no mandated null-prototype container or setSafeObjectProperty denylist, so `__proto__`/`constructor`/`prototype` keys reach Object.prototype.

Prevent recurrence

No object may be indexed/assigned with an untrusted key name unless the target is a null-prototype object or the write goes through the shared safe-set primitive that drops __proto__/constructor/prototype; parser name-mapping onto plain objects must run a name sanitizer.

DateFindingSeverityAreaFix
2026-03-25GHSA-mxrg-77hm-89hvCriticalPrototype Pollution in GSuiteAdmin node parameters leads to RCE2d9a2ec7
2026-04-22GHSA-hqr4-h3xv-9m3rCriticalXML Node Prototype Pollution to RCE85b77964
2026-04-22GHSA-q5f4-99jv-pgg5CriticalPrototype Pollution in XML Webhook Body Parser Leads to RCE85b77964
2026-05-13GHSA-c8xv-5998-g76hCriticalHTTP Request Node Pagination Prototype Pollution to RCE293d5afd
2026-05-13GHSA-wrwr-h859-xh2rCriticalXML Node Prototype Pollution Patch Bypass293d5afd
2026-06-02GHSA-x6p3-m6h9-fx7rHighMicrosoft SQL Node Prototype Pollutiondd4b3ff4
2026-06-10GHSA-2vff-hj5x-8gq7MediumPrototype Pollution enables confused-deputy execution via public webhooks71262655
2026-06-24GHSA-75qm-gp28-rcq9HighPrototype Pollution via Workflow Credentials Leads to Unauthenticated User and Project Enumerationa4bc50f9
2026-07-22GHSA-xwx6-jjhv-84p8HighPrototype Pollution via Dot-Notation Field Names Leads To Instance-Wide Denial of Service2222fe3a
n4
n8n8 findings

Credential / external-secret authorization enforced at the wrong altitude, bypassable via an equivalent uncovered path

Root cause

Credential-access and external-secret ($secrets) authorization checks were wired into one codepath (the editor REST controller, a resolution-time ownership check, or a pre-execution optimization keyed on a literal cred type) while semantically-equivalent paths — public API, inline sub-workflows, name-based resolution, expression-typed credential types, dynamic-node-params, and credential save-time — reached the credential/secret without the check.

Prevent recurrence

Credential decryption/use and external-secret resolution must be authorized against the acting user at a chokepoint common to ALL entry paths (execution, save, public API, inline sub-workflows, dynamic params); an authz check placed on only one of several equivalent paths fails review.

DateFindingSeverityAreaFix
2026-02-06GHSA-fxcw-h3qj-8m8pHighExternal Secrets Authorization Bypass in Credential Saving2b73ce42
2026-03-25GHSA-m63j-689w-3j35HighCredential Theft via Name-Based Resolution and Permission Checker Bypass in Community Edition2d9a2ec7
2026-04-21GHSA-r4v6-9fqc-w5jrHighCredential Authorization Bypass in dynamic-node-parameters Allows Foreign API Key Replayac411127
2026-06-10GHSA-pmqw-72cg-wx85HighCredential Exfiltration via Permission Bypass71262655
2026-06-24GHSA-2434-3x6q-8r99MediumExternal Secrets Accessible via Workflow Expressions Outside Credentialsa4bc50f9
2026-06-24GHSA-jp7m-xcgx-57qmMediumExternal Secrets Permission Bypass via Expression Parser Mismatcha4bc50f9
2026-07-22GHSA-6qc9-mqvw-jg7xHighCredential Authorization Bypass via Expression in HTTP Request Node genericAuthType2222fe3a
2026-07-22GHSA-cj9h-qx8g-pq2gHighShared-Workflow Editor Can Exfiltrate Credentials via Inline Sub-Workflow JSON2222fe3a
n5
n8n8 findings

n8n file-access sandbox (isFilePathBlocked/resolvePath) incomplete or bypassed at filesystem-capable node operands

Root cause

n8n has a single file-access guard (resolvePath + isFilePathBlocked over RESTRICT_FILE_ACCESS_TO / blocked patterns) living in the core FileSystem helper, but it was (a) lexically incomplete (no symlink canonicalization / TOCTOU-safe placement / .git denylist) and (b) not called by every node/operand that touches the filesystem, so nodes doing their own fs/spawn reached the disk unchecked.

Prevent recurrence

No node may pass an author-controlled path to a filesystem read/write/spawn syscall without first routing it through the shared realpath-canonicalizing isFilePathBlocked guard; adding a new fs-touching operand requires wiring that guard.

DateFindingSeverityAreaFix
2025-08-04GHSA-ggjm-f3g4-rwmmMediumSymlink traversal vulnerability in "Read/Write File" node allows access to restricted filesc2c3e08c
2025-11-26GHSA-v364-rw7m-3263CriticalRCE via Arbitrary File Writea49b179e
2025-12-22GHSA-x2mw-7j39-93xqCriticalArbitrary Command Execution via File Write and Git Operationse22acaab
2026-01-20GHSA-gfvg-qv54-r4pcCriticalImproper File Access Controls Allow Arbitrary File Read by Authenticated Usersfa6ad9ce
2026-05-05GHSA-2vx9-7wpg-88jqMediumLegacy ExecuteWorkflow Node Bypassed File Path Restrictions7277566c
2026-05-27GHSA-5xp3-2w67-427vMediumGit Node Clone and Push Operations Bypass File Sandboxd4f92238
2026-07-08GHSA-g3r5-9h93-4j2cHighRace Condition in Git Clone Node Allows Authenticated Users to Achieve Remote Code Execution46c1a0e8
2026-07-22GHSA-gf29-4f56-r2jfHighGit Node fetch/pull/pushTags Operations Bypass Sandbox Path Restriction2222fe3a
n6
n8n8 findings

Outbound HTTP not funnelled through one egress/domain-restriction guard

Root cause

The credential 'Allowed HTTP Request Domains' restriction and the SSRF egress filter are enforced opt-in inside the imperative HTTP Request node helper, so any surface that builds its own request pipeline (RoutingNode/declarative, GraphQL, AI/LLM nodes, agent MCP connector, MCP Client transport, per-node URL threading, dynamic-node-params routing) reaches the network without consulting them.

Prevent recurrence

No node or service may issue an outbound HTTP request carrying a credential (or reachable from workflow input) without the request URL and every redirect hop being validated by the central egress guard; bespoke fetch/undici transports must accept and honor the injected egress filter.

DateFindingSeverityAreaFix
2025-11-18GHSA-2xcx-75h9-vr9hMediumDomain allowlist bypass enables credential exfiltration404640f1
2026-05-05GHSA-3875-8gcx-7v46MediumCredential exfiltration via Allowed HTTP Request Domains Bypass8551b1b9
2026-06-09GHSA-rm2v-h48j-895mHighSecurityScorecard Node Leaks API Token to User-Controlled Host71262655
2026-06-24GHSA-h44j-f5r5-ph73High"Allowed HTTP Request Domains" Restriction Bypass via AI Agents MCP Connectora4bc50f9
2026-07-08GHSA-9w78-79q7-r4fpMediumAuthenticated SSRF via Dynamic Node Parameters Endpoints Allows Internal Network Access46c1a0e8
2026-07-08GHSA-gq66-9cw5-j5jmMediumGraphQL Node Bypasses "Allowed HTTP Request Domains" Restriction46c1a0e8
2026-07-22GHSA-64xh-79j6-r5v8HighBypass "Allowed HTTP Request Domains" Credential Restriction in Multiple AI and LLM Nodes2222fe3a
2026-07-22GHSA-vhf8-cg2h-cg3pMediumSSRF Protection Bypass via MCP Client Node2222fe3a
n7
n8n6 findings

Workflow expression evaluation sandbox mediates untrusted expressions by incomplete AST/accessor enumeration

Root cause

n8n evaluates untrusted workflow expressions in-process (Tournament/legacy AST-rewriting sandbox over a shared JS realm, and the isolated-vm host-bridge) where safety depends on enumerating every dangerous syntactic form / rebinding `this` / guarding every host accessor; any un-enumerated construct (function-expression this, spreads, reserved-name shadowing, `with`, arrow bodies, callee, an unguarded host accessor) leaks a real global -> RCE/pollution.

Prevent recurrence

Expression-sandbox safety must not rest on an enumerate-the-bad denylist or on one of several parallel accessors carrying a guard; every AST context and every host bridge accessor must route through the same complete mediation, and adding a new accessor/AST case requires proving it cannot reach a host global.

DateFindingSeverityAreaFix
2025-11-19GHSA-v98v-ff95-f3cpCriticaln8n Remote Code Execution via Expression Injection08f33201
2026-01-23GHSA-6cqr-8cfr-67f8CriticalExpression Escape Vulnerability Leading to RCEefc6c123
2026-02-25GHSA-vpcf-gvg4-6qwrCriticalExpression Sandbox Escape Leading to RCE562d8674
2026-07-08GHSA-pm35-fqvh-cq5gHighLegacy Expression Evaluator Sanitizer Bypass Leads to Authenticated Code Execution46c1a0e8
2026-07-22GHSA-gv7g-jm28-cr3mHighExpression sandbox escape via arrow-function bodies enabling command execution2222fe3a
2026-07-22GHSA-hx4h-vr3m-45vhMediumPrototype Pollution via VM Expression Engine Sandbox Escape Leads to Denial of Service2222fe3a
n8
n8n6 findings

editor-ui flows persisted/attacker-influenced URLs and HTML into DOM navigation/render sinks without a shared safe-output primitive

Root cause

The frontend passes attacker-influenced strings (OAuth authUrl, resource-locator cachedResultUrl, MCP client_name, markdown attributes, login redirect) into DOM navigation/render sinks (window.open, href, innerHTML/dangerouslyUseHTMLString, location) validated by ad-hoc per-site string checks (startsWith, a mis-anchored regex, unquoted attribute emission) rather than one shared safe-URL/safe-HTML primitive.

Prevent recurrence

No editor-ui code may pass a non-constant URL to a navigation sink without the shared scheme+origin allowlist, nor emit an attacker-influenced value into HTML without the shared safe-attribute serializer; per-call startsWith/regex URL checks fail review.

DateFindingSeverityAreaFix
2025-06-11GHSA-5vj6-wjr7-5v9fMediumOpen Redirect Vulnerability in n8n Login Flow4865d1e3
2025-12-23GHSA-qpq4-pw7f-pp8wHighStored Cross-Site Scripting via Markdown Rendering in Workflow UI3b9203e9
2026-02-06GHSA-364x-8g5j-x2prMediumXSS in Credential Management Flowe1a1e870
2026-04-22GHSA-537j-gqpc-p7fqHighXSS via MCP OAuth client85b77964
2026-04-22GHSA-f6x8-65q6-j9m9MediumOpen Redirect in MCP OAuth Consent Flow85b77964
2026-07-08GHSA-9wcp-9r3j-383qHighStored DOM XSS via Resource Locator cachedResultUrl46c1a0e8
n9
n8n6 findings

Resource resolved by raw id with no per-user object-level (BOLA/IDOR) authorization

Root cause

Endpoints (OAuth callback/reconnect, dynamic-credential EE, public-API variables, MCP-trigger consent, agent tools) fetch the target workflow/credential/variable by raw id via unscoped finders, authenticating the caller but never checking the caller's ownership/scope on the specific object, because the shared resource abstractions modeled no per-user authorize().

Prevent recurrence

No handler may act on a resource fetched by client-supplied id without a per-user object-level authorization check (ownership or the required scope on THAT object); raw repository get-by-id in a request path is forbidden.

DateFindingSeverityAreaFix
2026-02-10GHSA-vpgc-2f6g-7w7xMediumAuthorization Bypass in OAuth Callback via N8N_SKIP_AUTH_ON_OAUTH_CALLBACK7108ef45
2026-04-22GHSA-756q-gq9h-fp22MediumPublic API Variables IDOR Allows Cross-Project Secret Disclosure2d9a2ec7
2026-05-13GHSA-6h4j-wcr9-2vg7HighCross-user Authorization Bypass in Dynamic Credential OAuth Endpoints293d5afd
2026-06-10GHSA-2j5h-858j-5mpfHighCross-Tenant Credential Takeover via Dynamic Credentials EE Endpoints71262655
2026-07-08GHSA-q5xf-xhwf-cwqfMediumMember-Level Users Can Execute Other Users' MCP Server Trigger Workflows via Missing OAuth Authorization Check46c1a0e8
2026-07-08GHSA-x5vx-c2c8-m3w9HighAI Agents Project Viewer Privilege Escalation via run_node_tool46c1a0e8
n10
n8n6 findings

Python Code node isolation is a static AST/attribute denylist that is structurally incomplete

Root cause

User Python is confined by a hand-maintained static AST SecurityValidator plus a BLOCKED_ATTRIBUTES/BLOCKED_NAMES denylist (even after moving to the out-of-process native runner, that runner's gate IS the denylist), so any un-visited AST form (Constant/MatchClass/Global/FunctionDef/format-strings) or un-listed introspection dunder reaches a runtime escape.

Prevent recurrence

Python sandbox safety must not depend on completeness of a syntactic denylist; new language constructs and format/introspection entry points must fail closed (validated at runtime), and the executing process must not hold host capabilities the denylist is trying to seal.

DateFindingSeverityAreaFix
2025-12-08GHSA-62r4-hw23-cc8vCriticalArbitrary Command Execution in Pyodide based Python Code Nodea4757cf0
2026-01-29GHSA-8398-gmmx-564hCriticalPython sandbox escape76b4604a
2026-02-25GHSA-mmgg-m5j7-f83hCriticalPython Code Node Sandbox Escape562d8674
2026-04-22GHSA-44v6-jhgm-p3m4HighPython Task Runner Sandbox Escape85b77964
2026-05-27GHSA-9pq8-m8gp-4p53HighPython sandbox escaped4f92238
2026-06-10GHSA-jwm3-qcfw-c5ppMediumPython Code Node AST Validator Bypass71262655
n11
n8n5 findings

External IdP/directory identity claim trusted for account-linking, provisioning, or scope-granting without verification

Root cause

n8n binds a local account or grants a role/scope based on an external identity provider's claim (OIDC sub, email, IdP-mapped role) without verifying the claim legitimately and uniquely establishes that binding — no (issuer,subject) composite key, no email-verified/uniqueness check, no role ceiling, and scopes hard-coded rather than role-derived.

Prevent recurrence

An external IdP claim (subject, email, role) may bind to or elevate a local account only after issuer-scoping, email verification/uniqueness, and a role ceiling are enforced; treating a validly-signed claim as sufficient proof of account ownership or role fails review.

DateFindingSeverityAreaFix
2025-11-18GHSA-c545-x2rh-82fcHighLDAP Email-Based Account Linking Allows Privilege Escalation and Account Takeoverb3af602e
2026-06-24GHSA-mq3m-f8x3-579wHighCross-Issuer Token Exchange Account Binding via Subject-Only Identity Resolutiona4bc50f9
2026-07-08GHSA-35q8-9mj6-wjmfHighSSO Instance-Role Provisioning Allows Privilege Escalation to Instance Owner46c1a0e8
2026-07-08GHSA-777w-rpr6-c52hHighPrivilege Escalation and Code Execution via Full Public API Key Scope Assignment to Token Exchange JWTs46c1a0e8
2026-07-22GHSA-8342-988q-86crHighAccount Takeover via Unverified Email Claim in Token Exchange Embed Login2222fe3a
n12
n8n5 findings

Git node invokes git without constraining git's dangerous execution features (hooks, protocols, args, env, repo discovery)

Root cause

The Git node constructs the simple-git/child_process invocation and forwards author-controlled parameters and a working tree populated from an untrusted repo, without constraining git's code-executing surfaces: hooks run by default, free-form config keys, no `--` arg terminator, a prototype-inheriting env object, unrestricted transports, and automatic bare-repo discovery.

Prevent recurrence

Every git subprocess spawned by n8n must be constructed by the hardened invocation builder that disables hooks, pins repository discovery, terminates arguments, sanitizes the environment, and restricts transports; direct simpleGit()/spawn of git elsewhere is forbidden.

DateFindingSeverityAreaFix
2025-09-22GHSA-xgp7-7qjq-vg47HighRemote Code Execution via Git Node Pre-Commit Hook5bf3db5b
2025-11-14GHSA-wpqc-h9wp-chmqCriticalRemote Code Execution via Git Node Custom Pre-Commit Hook4dd853b2
2026-01-02GHSA-9g95-qf3f-ggrwCriticalOS Command Injection in Git Node503f2990
2026-05-13GHSA-57g9-58c2-xjg3CriticalArbitrary File Read via Git Node293d5afd
2026-07-22GHSA-rcv6-pvrj-4xcgHighAuthenticated code execution in the n8n Git node2222fe3a
Summarize with AI
ChatGPTPerplexityGeminiGrokClaude