cloneMessage does not isolate Buffer payloads when a message fans out
Current Behavior
When a node output is wired to more than one destination, the first destination receives the original message and each later one receives redUtil.cloneMessage(msg). That clone is what keeps the branches independent.
cloneMessage deep-clones with lodash.clonedeep, which clones a Buffer using buffer.slice(). buffer.slice() returns a view over the same memory rather than a copy, so the clone shares its bytes with the original. Two branches that mutate a Buffer payload in place therefore corrupt each other, silently and with no error raised.
Only Buffer is affected. Uint8Array, Float64Array, ArrayBuffer and DataView are all copied correctly by lodash.
The clone itself:
const clonedeep = require("lodash.clonedeep")
const orig = { payload: Buffer.from([1, 2, 3, 4]) }
const copy = clonedeep(orig)
copy.payload[0] = 99
orig.payload[0] // 99
orig.payload.buffer === copy.payload.buffer // trueExpected Behavior
A cloned message owns its own bytes. Mutating a Buffer payload in one branch leaves the other branch untouched.
Steps To Reproduce
- Import the flow below and deploy.
- It injects once on start, builds
Buffer.from([1,2,3,4]), and fans that message out to two function nodes. - Branch A does
msg.payload[0] = 99. Branch B only reads its own copy.
Branch B logs B SEES 99. It should log B SEES 1, since B holds a clone.
Example flow
[{"id":"t1","type":"tab","label":"buffer fan-out repro"},{"id":"inj","type":"inject","z":"t1","name":"start","props":[{"p":"payload"}],"payload":"","payloadType":"date","once":true,"onceDelay":0.2,"x":110,"y":80,"wires":[["mk"]]},{"id":"mk","type":"function","z":"t1","name":"make buffer","func":"msg.payload = Buffer.from([1,2,3,4]);\nreturn msg;","outputs":1,"x":260,"y":80,"wires":[["mutate","read"]]},{"id":"mutate","type":"function","z":"t1","name":"A mutates in place","func":"msg.payload[0] = 99;\nreturn msg;","outputs":1,"x":460,"y":50,"wires":[[]]},{"id":"read","type":"function","z":"t1","name":"B reads its own copy","func":"node.warn('B SEES ' + msg.payload[0] + ' (expected 1)');\nreturn msg;","outputs":1,"x":470,"y":110,"wires":[[]]}]Observed on current main:
[warn] [function:B reads its own copy] B SEES 99 (expected 1)Environment
- Node-RED version:
mainat d9644c41b (5.0.7) - Node.js version: v22.22.1 and v26.0.0, same result on both
- npm version: 10.9.8
- Platform/OS: Linux container and macOS
- Browser: not applicable, this is runtime side
Note on disclosure
Raising this in public rather than through the security process: triggering it requires flow-authoring rights, which already allow arbitrary code in a function node, so no privilege boundary is crossed. The impact is integrity only, within a single trust domain, and an external sender cannot steer the corruption. If the project reads it differently, it can move to [email protected].
Source: node-red/node-red