Skip to content

Commit 6993a19

Browse files
JkaviaJanardan S Kaviaautofix-ci[bot]
authored
feat: Add SSRF protection with DNS rebinding prevention (#13016)
* feat: Add SSRF protection with DNS rebinding prevention - Implement DNS pinning to prevent DNS rebinding attacks - Enable SSRF protection by default (ssrf_protection_enabled = True) - Add validate_and_resolve_url() function for DNS pinning - Create SSRFProtectedTransport for custom HTTP transport - Add comprehensive test suite for DNS rebinding protection - Change from warn_only=True to warn_only=False for enforcement This fixes a HIGH severity SSRF vulnerability (CVSS 8.6) that could allow attackers to bypass SSRF protection using DNS rebinding attacks to access: - Internal services and private networks - Cloud metadata endpoints (AWS, GCP, Azure) - Localhost services The DNS pinning implementation ensures that the IP address validated during security checks is the same IP used for the actual HTTP request, eliminating the TOCTOU (Time-of-Check to Time-of-Use) vulnerability. Changes: - src/lfx/src/lfx/components/data_source/api_request.py: Integrate DNS pinning - src/lfx/src/lfx/utils/ssrf_protection.py: Add validate_and_resolve_url() - src/lfx/src/lfx/utils/ssrf_transport.py: Custom transport with DNS pinning - src/lfx/src/lfx/services/settings/base.py: Enable SSRF protection by default - src/backend/tests/unit/components/data_source/test_dns_rebinding.py: Test suite Tested and verified working in GUI with DNS pinning logs visible. * fix(security): implement network-level DNS pinning for SSRF protection with HTTPS support This commit implements proper DNS rebinding protection that works with both HTTP and HTTPS by using network-level DNS pinning instead of URL rewriting. ## Changes ### Security Fixes - **ssrf_protection.py**: Fixed critical vulnerability where IP validation returned early on first allowlisted IP, skipping validation of remaining IPs. Now validates ALL resolved IPs before making decisions. - Blocks hostname if ANY resolved IP is blocked (even if others are allowlisted) - Supports partial allowlisting (uses only allowlisted IPs for DNS pinning) - Updated documentation to reflect that SSRF protection is now enabled by default - Changed warn_only default from True to False for stricter security ### DNS Pinning Implementation - **ssrf_transport.py**: Created DNSPinningNetworkBackend class that implements network-level DNS pinning - Extends httpcore.AsyncNetworkBackend to intercept TCP connections - Connects to pinned IP at network layer while preserving hostname for TLS - Works with both HTTP and HTTPS (fixes HTTPS regression from URL rewriting approach) - Properly handles TLS SNI and certificate verification ### Testing - **test_dns_rebinding.py**: Updated all DNS rebinding tests to verify network-level behavior - Tests now patch AutoBackend.connect_tcp to capture actual IP connections - Verifies DNS pinning prevents rebinding attacks - Verifies hostname preservation for TLS - Tests IPv6 support - All 7 tests passing ## Technical Details The previous URL rewriting approach (replacing hostname with IP in URL) broke HTTPS because: 1. TLS certificate is issued for hostname, not IP address 2. Certificate verification fails when connecting to IP 3. This was a regression - HTTPS worked before but was vulnerable The new network-level approach: 1. Resolves DNS during validation and pins IPs 2. Uses custom AsyncNetworkBackend to intercept TCP connections 3. Connects to pinned IP at network layer 4. Preserves original hostname for TLS SNI and certificate verification 5. Works transparently with both HTTP and HTTPS ## Function Clarification Two validation functions exist for different purposes: - validate_url_for_ssrf(): Simple validation for URL component (no HTTP requests) - validate_and_resolve_url(): Validation + DNS pinning for API Request component (makes HTTP requests) Fixes DNS rebinding vulnerability while maintaining HTTPS compatibility. * fix(security): support multiple IPs for dual-stack and load-balanced hosts This commit fixes DNS pinning to properly handle hosts that resolve to multiple IP addresses (dual-stack IPv4/IPv6, load balancing, etc.). ## Changes ### DNS Pinning Enhancement - **ssrf_transport.py**: Updated to accept and use list of validated IPs - Changed pinned_ips from dict[str, str] to dict[str, list[str]] - DNSPinningNetworkBackend now tries IPs in order with fallback - Supports dual-stack (IPv4+IPv6) and load-balanced hosts - Falls back to next IP if connection fails - **api_request.py**: Pass all validated IPs instead of just first one - Changed from validated_ips[0] to validated_ips - Enables proper failover for unreachable IPs ### Testing - **test_dns_rebinding.py**: Added test_dns_pinning_with_multiple_ips_fallback - Verifies system tries multiple IPs when first fails - Tests dual-stack scenario (IPv4 fails, IPv6 succeeds) - Confirms IPs are tried in order - All 8 tests passing ## Technical Details **Previous behavior:** - Only used first validated IP (validated_ips[0]) - Failed if that single IP was unreachable - Broke dual-stack and load-balanced hosts **New behavior:** - Accepts full list of validated IPs - Tries each IP in order until one succeeds - Properly supports: - Dual-stack hosts (IPv4 + IPv6) - Load-balanced hosts (multiple IPs) - Failover scenarios This maintains security (all IPs validated during SSRF check) while improving reliability and compatibility with modern networking configurations. * fix: improve SSRF protection with DNS pinning and fix resource leak - Implement network-level DNS pinning to prevent DNS rebinding attacks - Fix resource leak in SSRFProtectedTransport by not calling super().__init__() - Simplify error messages in SSRF protection (remove verbose explanations) - Restore warn_only parameter in validate_url_for_ssrf() for backward compatibility - Remove warn_only from validate_and_resolve_url() (not needed in API Request) - Switch from private AutoBackend to public httpcore.AnyIOBackend() API - Add noqa comments for unavoidable FBT and B008 warnings (matching parent class signature) - Support dual-stack (IPv4/IPv6) and load-balanced hosts with multiple IPs - Move imports to top of file for better code organization * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * test: fix SSRF protection test mocks to use validate_and_resolve_url - Updated test mocks from validate_url_for_ssrf to validate_and_resolve_url - Fixed test_ssrf_protection_disabled_by_default to explicitly disable protection - All 12 SSRF protection tests now passing * fix: read SSRF allowlist directly from environment for test compatibility - Modified get_allowed_hosts() to read from os.getenv() first - Fixes test isolation issue where settings service cached old values - Ensures patch.dict() in tests properly overrides allowlist - Maintains backward compatibility with settings service fallback * test: fix test_ssrf_protection_enforcement_mode to properly mock validation The test was failing because it wasn't mocking validate_and_resolve_url, so the actual SSRF protection wasn't being triggered. Updated the test to: - Mock validate_and_resolve_url to raise SSRFProtectionError - Follow the same pattern as other SSRF protection tests - Properly test that API Request component enforces blocking (no warn-only mode) * refactor: rewrite SSRF protection tests to test real implementation BREAKING CHANGE: Completely rewrote SSRF protection tests to properly test actual security behavior instead of mocking core functions. Changes: - Removed all mocks of validate_and_resolve_url() - tests now verify real SSRF blocking - Added tests that verify DNS pinning actually prevents rebinding attacks - Test real blocking of private IPs (127.0.0.1, 192.168.x.x, 10.x.x.x, 172.16.x.x) - Test real blocking of cloud metadata endpoints (169.254.169.254) - Verify allowlist functionality with actual hostnames, IPs, and CIDR ranges - Test that custom DNS pinning transport is used when protection is enabled - Test that normal httpx client is used when protection is disabled - Fixed environment variable caching in is_ssrf_protection_enabled() Why this matters: The old tests were mocking validate_and_resolve_url(), which meant they weren't actually testing if SSRF protection works. They were just testing error handling. Real SSRF vulnerabilities could have slipped through. The new tests: 1. Let the real SSRF protection code run 2. Verify actual private IPs are blocked 3. Verify public URLs are allowed 4. Verify allowlist bypasses work correctly 5. Verify DNS pinning transport is actually used All 41 tests pass (37 passed, 4 skipped for version checks). --------- Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 60627be commit 6993a19

14 files changed

Lines changed: 1172 additions & 169 deletions

File tree

src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1327,7 +1327,7 @@
13271327
},
13281328
{
13291329
"name": "googleapiclient",
1330-
"version": "2.194.0"
1330+
"version": "2.195.0"
13311331
}
13321332
],
13331333
"total_dependencies": 4

src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1781,7 +1781,7 @@
17811781
},
17821782
{
17831783
"name": "googleapiclient",
1784-
"version": "2.194.0"
1784+
"version": "2.195.0"
17851785
}
17861786
],
17871787
"total_dependencies": 7

src/backend/base/langflow/initial_setup/starter_projects/Pokédex Agent.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@
949949
},
950950
{
951951
"name": "googleapiclient",
952-
"version": "2.194.0"
952+
"version": "2.195.0"
953953
}
954954
],
955955
"total_dependencies": 4

src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2641,7 +2641,7 @@
26412641
},
26422642
{
26432643
"name": "googleapiclient",
2644-
"version": "2.194.0"
2644+
"version": "2.195.0"
26452645
}
26462646
],
26472647
"total_dependencies": 4

src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1643,7 +1643,7 @@
16431643
},
16441644
{
16451645
"name": "googleapiclient",
1646-
"version": "2.194.0"
1646+
"version": "2.195.0"
16471647
}
16481648
],
16491649
"total_dependencies": 4

src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@
274274
},
275275
{
276276
"name": "googleapiclient",
277-
"version": "2.194.0"
277+
"version": "2.195.0"
278278
},
279279
{
280280
"name": "lfx",

src/backend/tests/unit/components/data_source/test_api_request_component.py

Lines changed: 188 additions & 86 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)