Back to Blog
critical SEVERITY6 min read

How plaintext credential storage happens in Node-RED custom nodes and how to fix it

A critical security vulnerability was discovered in the `telegrambot/99-telegrambot.html` file where the Telegram bot token was registered as a Node-RED credential of type `'text'` instead of `'password'`. This meant the token was transmitted in plaintext over the admin API and stored without obfuscation in `flows_cred.json`, allowing attackers with filesystem or network access to recover the bot token and take full control of the Telegram bot.

O
By Orbis AppSec
Published July 26, 2026Reviewed July 26, 2026

Answer Summary

This is a plaintext credential storage vulnerability (CWE-256) in a Node-RED custom node (JavaScript/HTML) where a Telegram bot API token was declared with credential type `'text'` instead of `'password'` in the node's registration. The fix changes the credential type declaration from `{ type: 'text' }` to `{ type: 'password' }` in `telegrambot/99-telegrambot.html:67`, which ensures Node-RED treats the token as a secret — encrypting it at rest, masking it in the editor, and preventing it from being sent back to the client over the admin API.

Vulnerability at a Glance

cweCWE-256
fixChanged credential type from 'text' to 'password' in the RED.nodes.registerType() call
riskFull Telegram bot takeover via exposed API token
languageJavaScript/HTML (Node-RED)
root causeCredential declared as type 'text' instead of 'password' in node registration
vulnerabilityPlaintext credential storage in Node-RED credential system

How Plaintext Credential Storage Happens in Node-RED Custom Nodes and How to Fix It

Introduction

In the telegrambot/99-telegrambot.html file at line 67, we discovered a critical credential storage vulnerability where the Telegram bot token was declared as a Node-RED credential of type 'text'. This single configuration choice — token: { type: 'text' } inside the RED.nodes.registerType() call — meant that the bot's API token was treated as non-sensitive data by the entire Node-RED platform.

This matters because the Telegram bot token is the single authentication factor for controlling a bot. Anyone who obtains it can read messages, send messages as the bot, access group chats, and potentially exfiltrate data from all conversations the bot participates in. For developers building Node-RED custom nodes, this vulnerability demonstrates how a seemingly minor type declaration can have catastrophic security implications.

The Vulnerability Explained

Node-RED has a built-in credential management system that differentiates between two types of stored values:

  • type: 'text' — Treated as non-sensitive. Stored in flows_cred.json, returned in full via the admin REST API, and visible in the editor.
  • type: 'password' — Treated as a secret. Encrypted at rest, masked in the editor UI, and never sent back to the client after initial storage.

Here's the vulnerable code from telegrambot/99-telegrambot.html:

credentials: {
    token: { type: 'text' },
},

By declaring the token as 'text', the node inadvertently opted out of Node-RED's security protections. This created two concrete attack vectors:

Attack Vector 1: Filesystem Access

An attacker who gains read access to the Node-RED user directory (via a path traversal vulnerability, misconfigured file permissions, a backup exposure, or container escape) can read flows_cred.json directly. With type: 'text', the token is stored without the encryption that Node-RED applies to 'password' type credentials.

Attack Vector 2: Admin API Exposure

Node-RED's admin API endpoint (GET /credentials/:type/:id) returns credential values to the editor. For 'text' type credentials, the full value is returned. If the admin API is exposed without authentication (a common misconfiguration in development or IoT deployments), any network-adjacent attacker can query the API and retrieve the bot token in cleartext.

Additionally, the PR description notes that a prior version (pre-v17.3.0) leaked the token in verbose logs, indicating a pattern of historical exposure that compounds the risk.

Real-world exploitation scenario:

  1. Attacker discovers a Node-RED instance with the Telegram bot node deployed (common in home automation, IoT, and notification systems).
  2. Attacker accesses flows_cred.json (via exposed .node-red directory, Docker volume misconfiguration, or backup file).
  3. Attacker extracts the Telegram bot token directly from the JSON file.
  4. Attacker calls https://api.telegram.org/bot<TOKEN>/getUpdates to read all recent messages.
  5. Attacker uses the token to impersonate the bot, send phishing messages to group members, or exfiltrate sensitive data.

The Fix

The fix is a single-character type change on line 67 of telegrambot/99-telegrambot.html:

Before (vulnerable):

credentials: {
    token: { type: 'text' },
},

After (fixed):

credentials: {
    token: { type: 'password' },
},

This change instructs Node-RED to:

  1. Encrypt the token at rest — When stored in flows_cred.json, the value is encrypted using Node-RED's credential secret key (configured via credentialSecret in settings.js).
  2. Mask the token in the editor — The UI displays the field as a password input, preventing shoulder-surfing.
  3. Never return the token via the admin API — Once stored, the API returns only a boolean has_token: true indicator, not the actual value. This eliminates the network-based exfiltration vector entirely.
  4. Prevent logging exposure — Since the platform knows this is a secret, it avoids including it in debug output or verbose logs.

The fix is minimal and behavior-preserving: the bot still receives and uses its token for Telegram API authentication, but the token is now handled as a secret throughout its lifecycle within Node-RED.

Prevention & Best Practices

For Node-RED custom node developers:

  1. Always use type: 'password' for secrets — Any credential field containing API keys, tokens, passwords, or other authentication material must be declared as 'password', never 'text'.

  2. Audit existing nodes — Search your node HTML files for type: 'text' in credential declarations and evaluate whether each field contains sensitive data.

  3. Set a strong credentialSecret — Node-RED's encryption of password-type credentials depends on the credentialSecret in settings.js. Ensure this is set to a strong, unique value in production.

  4. Restrict admin API access — Enable authentication on the Node-RED admin interface using adminAuth in settings. Never expose the admin API to untrusted networks.

  5. Use environment variables for tokens — Consider using ${ENV_VAR} syntax in Node-RED to inject tokens from environment variables rather than storing them in flows at all.

Relevant standards:
- CWE-256: Plaintext Storage of a Password
- CWE-522: Insufficiently Protected Credentials
- OWASP: Sensitive Data Exposure (A02:2021 – Cryptographic Failures)

Key Takeaways

  • Node-RED's credential type declaration is a security boundary — The difference between 'text' and 'password' is not cosmetic; it determines whether encryption, masking, and API protection are applied.
  • A Telegram bot token in flows_cred.json with type: 'text' is recoverable by any attacker with filesystem read access — no decryption needed.
  • The admin API returns full values for 'text' credentials — meaning unauthenticated admin API access (common in dev/IoT) directly exposes the token over the network.
  • Historical log leakage (pre-v17.3.0) compounds the risk — tokens that were stored as plaintext may have already been written to log files, backups, or monitoring systems.
  • One-line fixes can eliminate critical vulnerabilities — this demonstrates why security-focused code review of configuration declarations is as important as reviewing logic.

How Orbis AppSec Detected This

  • Source: The Telegram bot token entered the system via the Node-RED editor credential input form and was persisted via the RED.nodes.registerType() credential system.
  • Sink: The flows_cred.json file on disk and the Node-RED admin REST API endpoint (GET /credentials/telegram-bot/:id), both in telegrambot/99-telegrambot.html:67.
  • Missing control: The credential was declared as type: 'text' instead of type: 'password', bypassing Node-RED's built-in encryption-at-rest and API masking for secrets.
  • CWE: CWE-256 (Plaintext Storage of a Password)
  • Fix: Changed the credential type declaration from 'text' to 'password' to enable Node-RED's native credential encryption and prevent token exposure via the admin API.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability illustrates a subtle but critical class of security issues in Node-RED custom node development: the credential type declaration acts as a security policy decision, not merely a UI hint. By declaring the Telegram bot token as type: 'text', the node author inadvertently disabled encryption at rest, enabled plaintext API retrieval, and exposed the most sensitive piece of data in the entire node's configuration.

The fix — changing a single string from 'text' to 'password' — activates Node-RED's full credential protection stack. For any developer building Node-RED nodes that handle API keys, tokens, or passwords, this should serve as a clear reminder: always declare secrets as type: 'password' in your credential definitions.

References

Frequently Asked Questions

What is plaintext credential storage in Node-RED?

When a Node-RED custom node declares a credential with type 'text', the value is stored without encryption in flows_cred.json and transmitted in cleartext over the admin API, making it recoverable by anyone with file or network access.

How do you prevent plaintext credential storage in Node-RED nodes?

Always declare sensitive credentials (tokens, passwords, API keys) with `type: 'password'` in the node's `credentials` object within `RED.nodes.registerType()`. This ensures Node-RED encrypts the value at rest and never sends it back to the editor.

What CWE is plaintext credential storage?

CWE-256 (Plaintext Storage of a Password) covers situations where credentials are stored in a form that is directly readable, without cryptographic protection.

Is Node-RED's built-in credential encryption enough to prevent token exposure?

Node-RED's credential system only encrypts values declared as type 'password'. If you use type 'text', the system treats it as non-sensitive data — no encryption, no masking, and the value is returned in full via the admin API.

Can static analysis detect plaintext credential storage in Node-RED nodes?

Yes, static analysis tools can flag credential declarations using `type: 'text'` for fields with names like 'token', 'password', 'apiKey', or 'secret' in Node-RED HTML registration files.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #484

Related Articles

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript fetch() and how to fix it

A critical Server-Side Request Forgery vulnerability in `popup.js` allowed attackers to inject malicious URLs from scraped webpages directly into fetch() calls, potentially accessing internal network resources and AWS metadata endpoints. The fix adds URL validation to ensure only HTTP/HTTPS protocols are used and blocks requests to private IP ranges and localhost addresses.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in axios versions prior to 1.15.1 allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This meant HTTP requests intended to stay internal could be routed through an attacker-controlled proxy, potentially exposing sensitive data. The fix upgrades axios to version 1.15.1, which correctly validates URLs against NO_PROXY rules.

critical

How JSON request validation bypass happens in Node.js API handlers and how to fix it

A critical validation bypass in the `/api/agents/jobs` endpoint allowed attackers to send malformed JSON with arbitrary properties that could trigger prototype pollution or unexpected behavior. The fix added comprehensive validation checks including array detection and property existence verification to prevent malicious payloads from reaching downstream processing logic.

critical

How Server-Side Request Forgery happens in Node.js httpProxy.js and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `hubcmdui/routes/httpProxy.js` where the `/proxy/test` endpoint passed a user-controlled `testUrl` parameter directly to `axios.get()` without any validation. An attacker could exploit this to probe internal infrastructure — including AWS metadata endpoints, Redis instances, and private network ranges. The fix introduces a strict URL validation function that blocks private IP ranges, loopback addresses, and non-HTTP(S)

critical

How Remote Configuration Injection happens in JavaScript fetch() and how to fix it

A critical vulnerability in `js/config.js` allowed attackers to inject malicious configuration data through DNS spoofing or man-in-the-middle attacks. The application fetched remote JSON configuration from GitHub without validating the response structure, enabling arbitrary code execution through crafted payloads. A single validation check now ensures only properly-formed configuration objects are accepted.

high

How missing Dependabot cooldown configuration happens in GitHub Actions and how to fix it

A high-severity vulnerability was discovered in the `.github/dependabot.yml` configuration file where no cooldown period was set for dependency updates. This exposed the Node.js library and its downstream consumers to potentially malicious or unstable packages immediately after publication. The fix adds a 7-day cooldown period to all package ecosystem entries, providing a critical safety buffer before accepting newly published package versions.