Skip to content

[GHSA-2x8m-83vc-6wv4] Flowise: SSRF Protection Bypass (TOCTOU & Default Insecure)#7427

Merged
advisory-database[bot] merged 1 commit into
JLLeitschuh/advisory-improvement-7427from
JLLeitschuh-GHSA-2x8m-83vc-6wv4
Apr 18, 2026
Merged

[GHSA-2x8m-83vc-6wv4] Flowise: SSRF Protection Bypass (TOCTOU & Default Insecure)#7427
advisory-database[bot] merged 1 commit into
JLLeitschuh/advisory-improvement-7427from
JLLeitschuh-GHSA-2x8m-83vc-6wv4

Conversation

@JLLeitschuh
Copy link
Copy Markdown

Updates

  • Description

Comments
Cleanup formatting of the report

@github
Copy link
Copy Markdown
Collaborator

github commented Apr 17, 2026

Hi there @igor-magun-wd! A community member has suggested an improvement to your security advisory. If approved, this change will affect the global advisory listed at github.com/advisories. It will not affect the version listed in your project repository.

This change will be reviewed by our Security Curation Team. If you have thoughts or feedback, please share them in a comment here! If this PR has already been closed, you can start a new community contribution for this advisory

Copilot AI review requested due to automatic review settings April 17, 2026 19:46
@github-actions github-actions Bot changed the base branch from main to JLLeitschuh/advisory-improvement-7427 April 17, 2026 19:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the GitHub-reviewed advisory JSON for GHSA-2x8m-83vc-6wv4 (Flowise SSRF protection bypass) to improve report readability/formatting within the details field.

Changes:

  • Updates the advisory modified timestamp.
  • Reformats the details markdown (adds inline code formatting and a fenced TypeScript block).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"aliases": [],
"summary": "Flowise: SSRF Protection Bypass (TOCTOU & Default Insecure)",
"details": "### Summary\nThe core security wrappers (secureAxiosRequest and secureFetch) intended to prevent Server-Side Request Forgery (SSRF) contain multiple logic flaws. These flaws allow attackers to bypass the allow/deny lists via DNS Rebinding (Time-of-Check Time-of-Use) or by exploiting the default configuration which fails to enforce any deny list.\n\n\n### Details\nThe flaws exist in packages/components/src/httpSecurity.ts.\n\nDefault Insecure: If process.env.HTTP_DENY_LIST is undefined, checkDenyList returns immediately, allowing all requests (including localhost).\n\nDNS Rebinding (TOCTOU): The function performs a DNS lookup (dns.lookup) to validate the IP, and then the HTTP client performs a new lookup to connect. An attacker can serve a valid IP first, then switch to an internal IP (e.g., 127.0.0.1) for the second lookup.\n\n\n### PoC\nnsure HTTP_DENY_LIST is unset (default behavior).\n\nUse any node utilizing secureFetch to access http://127.0.0.1.\n\nResult: Request succeeds.\n\nScenario 2: DNS Rebinding\n\nAttacker controls domain attacker.com and a custom DNS server.\n\nConfigure DNS to return 1.1.1.1 (Safe IP) with TTL=0 for the first query.\n\nConfigure DNS to return 127.0.0.1 (Blocked IP) for subsequent queries.\n\nFlowise validates attacker.com -> 1.1.1.1 (Allowed).\n\nFlowise fetches attacker.com -> 127.0.0.1 (Bypass).\n\nRun the following for manual verification \n\n\"// PoC for httpSecurity.ts Bypasses\nimport * as dns from 'dns/promises';\n\n// Mocking the checkDenyList logic from Flowise\nasync function checkDenyList(url: string) {\n const deniedIPs = ['127.0.0.1', '0.0.0.0']; // Simplified deny list logic\n\n if (!process.env.HTTP_DENY_LIST) {\n console.log(\"⚠️ HTTP_DENY_LIST not set. Returning allowed.\");\n return; // Vulnerability 1: Default Insecure\n }\n\n const { hostname } = new URL(url);\n const { address } = await dns.lookup(hostname);\n\n if (deniedIPs.includes(address)) {\n throw new Error(`IP ${address} is denied`);\n }\n console.log(`✅ IP ${address} allowed check.`);\n}\n\nasync function runPoC() {\n console.log(\"--- Test 1: Default Configuration (Unset HTTP_DENY_LIST) ---\");\n // Ensure env var is unset\n delete process.env.HTTP_DENY_LIST;\n try {\n await checkDenyList('http://127.0.0.1');\n console.log(\"[PASS] Default config allowed localhost access.\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e.message);\n }\n\n console.log(\"\\n--- Test 2: 'private' Keyword Bypass (Logic Flaw) ---\");\n process.env.HTTP_DENY_LIST = 'private'; // User expects this to block localhost\n try {\n await checkDenyList('http://127.0.0.1');\n // In real Flowise code, 'private' is not expanded to IPs, so it only blocks the string \"private\"\n console.log(\"[PASS] 'private' keyword failed to block localhost (Mock simulation).\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e.message);\n }\n}\n\nrunPoC();\"\n\n\n### Impact\nConfidentiality: High (Access to internal services if protection is bypassed).\n\nIntegrity: Low/Medium (If internal services allow state changes via GET).\n\nAvailability: Low.",
"details": "### Summary\nThe core security wrappers (secureAxiosRequest and secureFetch) intended to prevent Server-Side Request Forgery (SSRF) contain multiple logic flaws. These flaws allow attackers to bypass the allow/deny lists via DNS Rebinding (Time-of-Check Time-of-Use) or by exploiting the default configuration which fails to enforce any deny list.\n\n\n### Details\nThe flaws exist in `packages/components/src/httpSecurity.ts`.\n\nDefault Insecure: If process.env.HTTP_DENY_LIST is undefined, checkDenyList returns immediately, allowing all requests (including localhost).\n\nDNS Rebinding (TOCTOU): The function performs a DNS lookup (dns.lookup) to validate the IP, and then the HTTP client performs a new lookup to connect. An attacker can serve a valid IP first, then switch to an internal IP (e.g., `127.0.0.1`) for the second lookup.\n\n\n### PoC\nEnsure `HTTP_DENY_LIST` is unset (default behavior).\n\nUse any node utilizing secureFetch to access `http://127.0.0.1`.\n\nResult: Request succeeds.\n\n#### Scenario 2: DNS Rebinding\n\nAttacker controls domain attacker.com and a custom DNS server.\n\nConfigure DNS to return `1.1.1.1` (Safe IP) with TTL=0 for the first query.\n\nConfigure DNS to return `127.0.0.1` (Blocked IP) for subsequent queries.\n\nFlowise validates `attacker.com` -> `1.1.1.1` (Allowed).\n\nFlowise fetches `attacker.com` -> `127.0.0.1` (Bypass).\n\nRun the following for manual verification \n\n```ts\n// PoC for httpSecurity.ts Bypasses\nimport * as dns from 'dns/promises';\n\n// Mocking the checkDenyList logic from Flowise\nasync function checkDenyList(url: string) {\n const deniedIPs = ['127.0.0.1', '0.0.0.0']; // Simplified deny list logic\n\n if (!process.env.HTTP_DENY_LIST) {\n console.log(\"⚠️ HTTP_DENY_LIST not set. Returning allowed.\");\n return; // Vulnerability 1: Default Insecure\n }\n\n const { hostname } = new URL(url);\n const { address } = await dns.lookup(hostname);\n\n if (deniedIPs.includes(address)) {\n throw new Error(`IP ${address} is denied`);\n }\n console.log(`✅ IP ${address} allowed check.`);\n}\n\nasync function runPoC() {\n console.log(\"--- Test 1: Default Configuration (Unset HTTP_DENY_LIST) ---\");\n // Ensure env var is unset\n delete process.env.HTTP_DENY_LIST;\n try {\n await checkDenyList('http://127.0.0.1');\n console.log(\"[PASS] Default config allowed localhost access.\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e.message);\n }\n\n console.log(\"\\n--- Test 2: 'private' Keyword Bypass (Logic Flaw) ---\");\n process.env.HTTP_DENY_LIST = 'private'; // User expects this to block localhost\n try {\n await checkDenyList('http://127.0.0.1');\n // In real Flowise code, 'private' is not expanded to IPs, so it only blocks the string \"private\"\n console.log(\"[PASS] 'private' keyword failed to block localhost (Mock simulation).\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e.message);\n }\n}\n\nrunPoC();\n```\n\n\n### Impact\nConfidentiality: High (Access to internal services if protection is bypassed).\n\nIntegrity: Low/Medium (If internal services allow state changes via GET).\n\nAvailability: Low.",
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated details text is internally inconsistent and the PoC is not reliably copy/paste runnable as TypeScript:

  • The narrative lists only “Default Insecure” and “DNS Rebinding (TOCTOU)”, but the PoC includes a second test for a 'private' keyword bypass. Either document this third issue in the Details section (with a brief explanation) or remove that test to avoid confusion.
  • The PoC uses catch (e) { ... e.message }; with common TS settings (useUnknownInCatchVariables) e is unknown and this won’t type-check. Use a type guard (e instanceof Error) or type the catch variable.
  • Minor formatting: “Run the following for manual verification ” has trailing whitespace and would read better with a colon before the code block.
Suggested change
"details": "### Summary\nThe core security wrappers (secureAxiosRequest and secureFetch) intended to prevent Server-Side Request Forgery (SSRF) contain multiple logic flaws. These flaws allow attackers to bypass the allow/deny lists via DNS Rebinding (Time-of-Check Time-of-Use) or by exploiting the default configuration which fails to enforce any deny list.\n\n\n### Details\nThe flaws exist in `packages/components/src/httpSecurity.ts`.\n\nDefault Insecure: If process.env.HTTP_DENY_LIST is undefined, checkDenyList returns immediately, allowing all requests (including localhost).\n\nDNS Rebinding (TOCTOU): The function performs a DNS lookup (dns.lookup) to validate the IP, and then the HTTP client performs a new lookup to connect. An attacker can serve a valid IP first, then switch to an internal IP (e.g., `127.0.0.1`) for the second lookup.\n\n\n### PoC\nEnsure `HTTP_DENY_LIST` is unset (default behavior).\n\nUse any node utilizing secureFetch to access `http://127.0.0.1`.\n\nResult: Request succeeds.\n\n#### Scenario 2: DNS Rebinding\n\nAttacker controls domain attacker.com and a custom DNS server.\n\nConfigure DNS to return `1.1.1.1` (Safe IP) with TTL=0 for the first query.\n\nConfigure DNS to return `127.0.0.1` (Blocked IP) for subsequent queries.\n\nFlowise validates `attacker.com` -> `1.1.1.1` (Allowed).\n\nFlowise fetches `attacker.com` -> `127.0.0.1` (Bypass).\n\nRun the following for manual verification \n\n```ts\n// PoC for httpSecurity.ts Bypasses\nimport * as dns from 'dns/promises';\n\n// Mocking the checkDenyList logic from Flowise\nasync function checkDenyList(url: string) {\n const deniedIPs = ['127.0.0.1', '0.0.0.0']; // Simplified deny list logic\n\n if (!process.env.HTTP_DENY_LIST) {\n console.log(\"⚠️ HTTP_DENY_LIST not set. Returning allowed.\");\n return; // Vulnerability 1: Default Insecure\n }\n\n const { hostname } = new URL(url);\n const { address } = await dns.lookup(hostname);\n\n if (deniedIPs.includes(address)) {\n throw new Error(`IP ${address} is denied`);\n }\n console.log(`✅ IP ${address} allowed check.`);\n}\n\nasync function runPoC() {\n console.log(\"--- Test 1: Default Configuration (Unset HTTP_DENY_LIST) ---\");\n // Ensure env var is unset\n delete process.env.HTTP_DENY_LIST;\n try {\n await checkDenyList('http://127.0.0.1');\n console.log(\"[PASS] Default config allowed localhost access.\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e.message);\n }\n\n console.log(\"\\n--- Test 2: 'private' Keyword Bypass (Logic Flaw) ---\");\n process.env.HTTP_DENY_LIST = 'private'; // User expects this to block localhost\n try {\n await checkDenyList('http://127.0.0.1');\n // In real Flowise code, 'private' is not expanded to IPs, so it only blocks the string \"private\"\n console.log(\"[PASS] 'private' keyword failed to block localhost (Mock simulation).\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e.message);\n }\n}\n\nrunPoC();\n```\n\n\n### Impact\nConfidentiality: High (Access to internal services if protection is bypassed).\n\nIntegrity: Low/Medium (If internal services allow state changes via GET).\n\nAvailability: Low.",
"details": "### Summary\nThe core security wrappers (secureAxiosRequest and secureFetch) intended to prevent Server-Side Request Forgery (SSRF) contain multiple logic flaws. These flaws allow attackers to bypass the allow/deny lists via DNS Rebinding (Time-of-Check Time-of-Use), by exploiting the default configuration which fails to enforce any deny list, or by using the unsupported `private` keyword in the deny list configuration.\n\n\n### Details\nThe flaws exist in `packages/components/src/httpSecurity.ts`.\n\nDefault Insecure: If process.env.HTTP_DENY_LIST is undefined, checkDenyList returns immediately, allowing all requests (including localhost).\n\n`private` Keyword Bypass: If a user sets `HTTP_DENY_LIST=private`, localhost and private network IPs are not blocked because the literal keyword is not expanded into the corresponding IP ranges. This can create a false sense of protection while requests to internal addresses are still allowed.\n\nDNS Rebinding (TOCTOU): The function performs a DNS lookup (dns.lookup) to validate the IP, and then the HTTP client performs a new lookup to connect. An attacker can serve a valid IP first, then switch to an internal IP (e.g., `127.0.0.1`) for the second lookup.\n\n\n### PoC\nEnsure `HTTP_DENY_LIST` is unset (default behavior).\n\nUse any node utilizing secureFetch to access `http://127.0.0.1`.\n\nResult: Request succeeds.\n\n#### Scenario 2: DNS Rebinding\n\nAttacker controls domain attacker.com and a custom DNS server.\n\nConfigure DNS to return `1.1.1.1` (Safe IP) with TTL=0 for the first query.\n\nConfigure DNS to return `127.0.0.1` (Blocked IP) for subsequent queries.\n\nFlowise validates `attacker.com` -> `1.1.1.1` (Allowed).\n\nFlowise fetches `attacker.com` -> `127.0.0.1` (Bypass).\n\nRun the following for manual verification:\n\n```ts\n// PoC for httpSecurity.ts Bypasses\nimport * as dns from 'dns/promises';\n\n// Mocking the checkDenyList logic from Flowise\nasync function checkDenyList(url: string) {\n const deniedIPs = ['127.0.0.1', '0.0.0.0']; // Simplified deny list logic\n\n if (!process.env.HTTP_DENY_LIST) {\n console.log(\"⚠️ HTTP_DENY_LIST not set. Returning allowed.\");\n return; // Vulnerability 1: Default Insecure\n }\n\n const { hostname } = new URL(url);\n const { address } = await dns.lookup(hostname);\n\n if (deniedIPs.includes(address)) {\n throw new Error(`IP ${address} is denied`);\n }\n console.log(`✅ IP ${address} allowed check.`);\n}\n\nasync function runPoC() {\n console.log(\"--- Test 1: Default Configuration (Unset HTTP_DENY_LIST) ---\");\n // Ensure env var is unset\n delete process.env.HTTP_DENY_LIST;\n try {\n await checkDenyList('http://127.0.0.1');\n console.log(\"[PASS] Default config allowed localhost access.\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e instanceof Error ? e.message : String(e));\n }\n\n console.log(\"\\n--- Test 2: 'private' Keyword Bypass (Logic Flaw) ---\");\n process.env.HTTP_DENY_LIST = 'private'; // User expects this to block localhost\n try {\n await checkDenyList('http://127.0.0.1');\n // In real Flowise code, 'private' is not expanded to IPs, so it only blocks the string \"private\"\n console.log(\"[PASS] 'private' keyword failed to block localhost (Mock simulation).\");\n } catch (e) {\n console.log(\"[FAIL] Blocked:\", e instanceof Error ? e.message : String(e));\n }\n}\n\nrunPoC();\n```\n\n\n### Impact\nConfidentiality: High (Access to internal services if protection is bypassed).\n\nIntegrity: Low/Medium (If internal services allow state changes via GET).\n\nAvailability: Low.",

Copilot uses AI. Check for mistakes.
@advisory-database advisory-database Bot merged commit 57bef51 into JLLeitschuh/advisory-improvement-7427 Apr 18, 2026
7 of 8 checks passed
@advisory-database
Copy link
Copy Markdown
Contributor

Hi @JLLeitschuh! Thank you so much for contributing to the GitHub Advisory Database. This database is free, open, and accessible to all, and it's people like you who make it great. Thanks for choosing to help others. We hope you send in more contributions in the future!

@advisory-database advisory-database Bot deleted the JLLeitschuh-GHSA-2x8m-83vc-6wv4 branch April 18, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants