CVE
CVE-2026-103251
CWE
CWE-862
Affected Surface
- n8n before 1.123.80, n8n 2.0.0 through 2.39.5, and n8n 2.40.0
- Self-hosted n8n deployments that use Redis-backed queue mode for scaling
- Instances with community packages enabled where Redis write access is broader than the fully trusted n8n control plane
CVE-2026-103251 is a good example of how supply-chain risk can show up in orchestration glue, not just in the package manager itself. n8n already had a normal path for installing community nodes through its UI and API. That path parsed the package name, checked whether the package was banned, enforced the verified-versus-unverified package policy, and could pin a checksum. Queue mode introduced a second control path through Redis pub-sub. In affected releases, that second path skipped the checks and went straight to package download and load.
The result was simple. If an attacker could write to the Redis bus used by a queue-mode n8n cluster, they could send a forged community-package install message and make every instance fetch and load an npm package of the attacker’s choice. The advisory rates that high severity because Redis write access is the prerequisite. The code path still matters because n8n treated the Redis message itself as a trusted install decision.
Affected versions
Affected package:
- n8n before 1.123.80
- n8n 2.0.0 through 2.39.5
- n8n 2.40.0
The bug matters when all of the following are true:
- the deployment uses Redis-backed queue mode;
- community packages are enabled;
- an attacker can publish or inject messages onto the Redis channel that n8n uses for internal command fan-out.
The advisory is about self-hosted clusters, not the hosted npm registry. Still, the effect is an npm supply-chain boundary failure inside the application: a message on the Redis bus becomes a cluster-wide package-selection primitive.
The normal install path did have the right checks
In the vulnerable 2.40.0 package, the public install path already performed several controls before package download:
const packageStatus = await this.communityPackagesService.checkNpmPackageStatus(name)
if (packageStatus.status !== 'OK') {
throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`)
}
installedPackage = await this.communityPackagesService.installPackage(
parsed.packageName,
packageVersion,
checksum
)
Inside the service layer, the trusted install path also enforced the instance policy for unverified packages and checksum verification:
this.checkInstallPermissions(shouldValidateChecksum)
if (options.checksum) {
await verifyIntegrity(packageName, packageVersion, this.getNpmRegistry(), options.checksum, authToken)
}
await checkIfVersionExistsOrThrow(packageName, packageVersion, this.getNpmRegistry(), authToken)
Those checks are exactly what defenders would expect around a feature that installs third-party npm code into a running automation platform.
Queue mode skipped the checks and trusted the bus
The queue-mode install flow was different. After a legitimate install, n8n published a cluster command through Redis:
void this.publisher.publishCommand({
command: isUpdate ? 'community-package-update' : 'community-package-install',
payload: { packageName, packageVersion: installedVersion },
})
On the receiving side, every worker subscribed to the same command name. In 2.40.0, the install event handler did not re-run the package checks. It accepted the package name and version from the pub-sub message and handed them directly to the internal installer:
async handleInstallEvent({ packageName, packageVersion }) {
await this.installOrUpdateNpmPackage(packageName, packageVersion)
}
async installOrUpdateNpmPackage(packageName, packageVersion) {
const authToken = this.getNpmAuthToken()
await this.downloadPackage(packageName, packageVersion, authToken)
await this.loadNodesAndCredentials.loadPackage(packageName)
}
That path skipped the protections the normal install route relied on:
- no re-parse of the package name;
- no reserved-name or scope validation;
- no “banned package” check;
- no checksum verification;
- no “unverified packages forbidden” gate;
- no lookup of an already-approved package record before installation.
From the advisories alone, it is easy to read this as “Redis write access can install packages.” The package code shows the more precise problem: Redis messages were treated as authoritative state transitions instead of as replication signals for decisions that had already been validated elsewhere.
One forged message was enough to fan out cluster-wide
The pub-sub subscriber groups community package commands by command name and package name:
if (
msg.command === 'community-package-install' ||
msg.command === 'community-package-update' ||
msg.command === 'community-package-uninstall'
) {
return `${eventName}:${msg.payload.packageName}`
}
That means the wire format the workers care about is small. A forged command only needs the expected command verb and a payload with the package identity the worker should act on. The subscriber code does filter out self-sent messages and targeted messages for a different host, but it does not authenticate the sender as a trusted control-plane component before dispatching the install handler.
Operationally, that turns Redis into the package-selection control plane for every connected instance.
This is package-load execution, not classic npm install-script malware
There is an important nuance in the actual package handling code. n8n does not simply run “npm install” and accept whatever lifecycle scripts do. The vulnerable build downloads the chosen tarball with “npm pack”, unpacks it, strips some dependency fields, and then installs with scripts disabled:
const tarOutput = await executeNpmCommand(
['pack', `${packageName}@${packageVersion}`, '--quiet'],
{ cwd: this.downloadFolder, registry, authToken }
)
await executeNpmCommand([
'install',
'--audit=false',
'--fund=false',
'--bin-links=false',
'--install-strategy=shallow',
'--ignore-scripts=true',
'--omit=dev'
], ...)
That is better than a raw lifecycle-script install, but it does not make the vulnerability harmless. The package is still loaded immediately afterward as a community node:
await this.loadNodesAndCredentials.loadPackage(packageName)
So the realistic execution surface is package code that runs during module load or node registration, not just “preinstall” or “postinstall” hooks. For defenders, that means a suspicious community package install should still be treated as attacker-supplied code execution on the n8n instance.
What 2.40.1 changed
The 2.40.1 build closes the gap in the place that mattered. First, it centralizes the registry and policy checks into a helper:
async runRegistryChecks(packageName, packageVersion, checksum) {
this.checkInstallPermissions(Boolean(checksum))
const packageStatus = await this.checkNpmPackageStatus(packageName)
if (packageStatus.status !== NPM_PACKAGE_STATUS_GOOD) {
throw new UnexpectedError(`Package "${packageName}" is not allowed`)
}
if (checksum) {
await verifyIntegrity(packageName, packageVersion, registry, checksum, authToken)
}
await checkIfVersionExistsOrThrow(packageName, packageVersion, registry, authToken)
}
Then it changes the pub-sub install handler so the message can no longer choose a fresh package and version on its own:
async handleInstallEvent({ packageName, packageVersion, checksum }) {
this.assertPackageChangesAllowed()
const parsed = this.parseNpmPackageName(packageName)
const installedPackage = await this.findInstalledPackage(parsed.packageName)
if (!installedPackage) {
throw new UnexpectedError('No installed package record found')
}
const resolvedVersion = installedPackage.installedVersion
await this.runRegistryChecks(
installedPackage.packageName,
resolvedVersion,
resolvedVersion === packageVersion ? checksum : undefined
)
await this.installOrUpdateNpmPackage(installedPackage.packageName, resolvedVersion)
}
That is the important design change. Redis is no longer deciding what package gets installed. It is only telling peers to converge on a package record that the trusted path already created. The same patch also tightens package-name parsing and validates that the resolved package directory stays inside the node_modules tree, which is a sensible extra hardening pass around attacker-controlled package identifiers.
The same patch train fixed a second n8n bug
The fixed releases 1.123.80, 2.39.6, and 2.40.1 also close CVE-2026-103250, the MongoDB Chat Memory issue tracked as GHSA-w24g-6454-7w7f. That second advisory is a separate bug class. It lets unauthenticated callers supply MongoDB query operators in a chat session identifier when a public Chat Trigger workflow is backed by MongoDB Chat Memory.
The reason to mention it here is operational, not thematic. Teams that defer the queue-mode package bug because “Redis is private” should still notice that the same n8n patch set also fixes a public-endpoint data access problem.
Scoping exposure
Start with the version and the deployment mode. If the cluster is not using Redis-backed queue mode, this CVE is not the one to prioritize. If it is, the next question is whether Redis was reachable by anything except the main n8n instance and its workers.
Then audit the community package posture:
npm ls n8n
Review whether the deployment allows ad hoc community packages from the UI, or whether it pins them from environment variables:
N8N_COMMUNITY_PACKAGES_MANAGED_BY_ENV=true
N8N_COMMUNITY_PACKAGES='[
{
"name": "n8n-nodes-foo",
"version": "1.2.3",
"checksum": "sha512-..."
}
]'
The environment-managed mode is not a complete defense by itself, but it is a stronger operating model because it turns package selection into declarative configuration and gives defenders a concrete allowlist to compare against the runtime state.
If you find evidence that an unexpected community package was installed through queue mode, treat the affected nodes as exposed to attacker-controlled code. At that point the question is not only “which package version was fetched?” It is also what secrets, workflow credentials, or downstream systems that node could reach once loaded.
Remediation
- Upgrade n8n to 1.123.80, 2.39.6, 2.40.1, or later.
- Restrict Redis access to trusted n8n components with authentication, private networking, and explicit ACLs.
- Disable community packages if they are not required.
- Prefer environment-managed community packages with explicit version and checksum pinning.
- Audit installed community nodes for anything unexpected and redeploy from a clean image if a suspicious package was ever loaded.
The key lesson in CVE-2026-103251 is narrow and useful. n8n had already implemented most of the right package-safety checks. The vulnerable branch simply failed to run them on the Redis-driven path that actually told the rest of the cluster what to install. That kind of split control plane is exactly where package management turns into an application-security problem.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.