Skip to content

fix(security): harden origin middleware against DNS rebinding attacks (#12860)#14085

Open
jiangyj545 wants to merge 1 commit into
Comfy-Org:masterfrom
jiangyj545:fix/dns-rebinding-middleware-12860
Open

fix(security): harden origin middleware against DNS rebinding attacks (#12860)#14085
jiangyj545 wants to merge 1 commit into
Comfy-Org:masterfrom
jiangyj545:fix/dns-rebinding-middleware-12860

Conversation

@jiangyj545
Copy link
Copy Markdown

Summary

Fixes #12860 — The create_origin_only_middleware in server.py is vulnerable to DNS rebinding attacks. An attacker-controlled domain that rebinds to 127.0.0.1 can bypass the existing Host/Origin check because both headers will contain the attacker's domain (which matches), while the actual TCP connection goes to localhost.

Root Cause

The current middleware only checks that Host == Origin for loopback addresses. In a DNS rebinding scenario:

  1. Browser resolves attacker.com → 127.0.0.1 (rebound)
  2. Request sent to http://attacker.com:8188/prompt with Host and Origin both set to attacker.com:8188
  3. Host == Origin check passes → request processed
  4. Attacker can now queue arbitrary ComfyUI workflows

Reference: huntr.com/bounties/f1458e43-64a7-4df2-b71c-9ca453755dc7 (NCC Group PoC)

Changes

1. CSRF Token (per-process, auto-generated)

  • Random uuid4().hex token generated once at first request
  • No config needed, no database, fully backward-compatible

2. Double-DNS-Resolution Check (_dns_rebind_check)

  • Resolves the Host header hostname twice with a 200ms delay
  • If the two IP sets differ → reject (DNS rebinding detected)
  • If any resolved IP is non-loopback → reject
  • Classic defense-in-depth technique (same approach as Chrome Local Network Access)

3. CSRF Validation for State-Changing Requests

  • POST/PUT/DELETE/PATCH requests from loopback origins must include the CSRF token
  • Accepted via X-CSRF-Token header or ?csrf_token= query parameter
  • Returns clear 403 if missing/invalid

4. Token Delivery

  • Every response includes Set-Cookie: comfyui_csrf= (SameSite=Strict, 7-day)
  • Also exposes X-CSRF-Token response header for API/non-browser clients

Backward Compatibility

  • GET/HEAD/OPTIONS: unchanged (no CSRF check for read-only)
  • Non-loopback requests: unchanged (existing external access preserved)
  • Same-origin UI: fully automatic (browser sends cookie transparently)
  • API scripts: read X-CSRF-Token from response header, include in subsequent requests
  • No new dependencies (stdlib only: uuid, socket, ipaddress, time)

Testing Scenarios

Scenario Before After
Normal same-origin UI use works works (auto cookie)
API script with CSRF header works works
DNS rebind attack (cross-site) VULNERABLE 403 blocked
CSRF without DNS rebind VULNERABLE 403 blocked
Cross-site via Sec-Fetch-Site 403 blocked 403 blocked (unchanged)
Host/Origin mismatch 403 blocked 403 blocked (unchanged)

…Comfy-Org#12860)

- Add CSRF token generation (per-process, sent as cookie + response header)
- Require CSRF token on state-changing requests (POST/PUT/DELETE/PATCH)
  from loopback origins — prevents cross-site request forgery via DNS rebinding
- Add double-DNS-resolution check (_dns_rebind_check): resolves the Host
  hostname twice with 200ms delay; rejects if addresses differ (rebind signal)
  or if any resolved IP is non-loopback
- Preserve all existing Sec-Fetch-Site and Host/Origin mismatch checks

CVE reference: huntr.com/bounties/f1458e43-64a7-4df2-b71c-9ca453755dc7
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds CSRF token and DNS rebinding defense mechanisms to the loopback-origin middleware in server.py. It introduces a per-process CSRF token (_csrf_token) lazily initialized via get_csrf_token(), and a _dns_rebind_check() function that resolves a hostname twice to detect DNS rebinding attacks. The middleware now validates CSRF tokens supplied via X-CSRF-Token header or csrf_token query parameter for state-changing requests (POST/PUT/DELETE/PATCH) from loopback origins, returning 403 on detection of DNS rebinding or invalid tokens. All requests processed by the middleware receive the CSRF token via both comfyui_csrf cookie and X-CSRF-Token response header.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main security improvement: hardening the origin middleware against DNS rebinding attacks, which is the primary objective of the changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the vulnerability, root cause, implementation approach, backward compatibility considerations, and testing scenarios.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
server.py (1)

167-169: 💤 Low value

DNS rebinding check only covers IPv4.

The function only resolves AF_INET addresses. An attacker could potentially use an IPv6-only DNS rebinding attack (rebinding to ::1) which wouldn't be detected by this check.

Consider adding AF_INET6 resolution to the check, or document why IPv6 is excluded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.py` around lines 167 - 169, The DNS rebinding check currently calls
socket.getaddrinfo(hostname, None, socket.AF_INET, ...) into r1/r2 which only
resolves IPv4; update the check to also resolve IPv6 (AF_INET6) and include
those results in the comparison (or call getaddrinfo with family=0 to return
both families), then compare the combined address sets from the two lookups for
changes; locate where r1, r2 and hostname are used in the function and ensure
the comparison logic handles both IPv4 and IPv6 entries (normalize/address-only
comparison) to detect rebinding to ::1 as well as 127.0.0.1.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server.py`:
- Around line 166-171: The DNS resolution and sleep are blocking: replace the
synchronous socket.getaddrinfo and time.sleep calls with non-blocking
equivalents by running getaddrinfo in an executor (e.g.,
asyncio.get_running_loop().run_in_executor or asyncio.to_thread) and use
asyncio.sleep(0.2) instead of time.sleep; ensure the surrounding function is
async (or the call site invokes an async helper) and preserve the
socket.gaierror handling and the logic producing r1 and r2 so the rest of the
code that uses r1/r2 remains unchanged.

---

Nitpick comments:
In `@server.py`:
- Around line 167-169: The DNS rebinding check currently calls
socket.getaddrinfo(hostname, None, socket.AF_INET, ...) into r1/r2 which only
resolves IPv4; update the check to also resolve IPv6 (AF_INET6) and include
those results in the comparison (or call getaddrinfo with family=0 to return
both families), then compare the combined address sets from the two lookups for
changes; locate where r1, r2 and hostname are used in the function and ensure
the comparison logic handles both IPv4 and IPv6 entries (normalize/address-only
comparison) to detect rebinding to ::1 as well as 127.0.0.1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a1a7b59b-5dbd-4eee-b6b5-5d1ca4b77889

📥 Commits

Reviewing files that changed from the base of the PR and between ea62dc1 and a694c3b.

📒 Files selected for processing (1)
  • server.py

Comment thread server.py
Comment on lines +166 to +171
try:
r1 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
time.sleep(0.2)
r2 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
except socket.gaierror:
return False # unresolvable → reject
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Blocking calls in async context will stall the event loop.

time.sleep(0.2) and socket.getaddrinfo() are synchronous blocking operations. When called from the async middleware, they block the entire aiohttp event loop for the duration, causing latency spikes and reducing throughput for all concurrent requests.

Run the blocking DNS work in a thread pool executor to avoid stalling the event loop:

Suggested fix using asyncio executor
+import asyncio
+
-def _dns_rebind_check(hostname):
+async def _dns_rebind_check(hostname):
     """Resolve *hostname* twice with a 200ms delay.

     Returns True if both resolutions yield the **same** set of loopback addresses.
     Returns False when the two results differ (classic DNS rebinding signal)
     or when any result contains a non-loopback IP.
     """
+    loop = asyncio.get_running_loop()
+
+    def _resolve():
+        try:
+            r1 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
+            time.sleep(0.2)
+            r2 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
+            return r1, r2
+        except socket.gaierror:
+            return None, None
+
     try:
-        r1 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
-        time.sleep(0.2)
-        r2 = socket.getaddrinfo(hostname, None, socket.AF_INET, socket.SOCK_STREAM)
-    except socket.gaierror:
-        return False  # unresolvable → reject
+        r1, r2 = await loop.run_in_executor(None, _resolve)
+        if r1 is None:
+            return False  # unresolvable → reject
+    except Exception:
+        return False

     ips1 = sorted({addr[4][0] for addr in r1})
     ...

Then update the call site at line 218:

-                if not _dns_rebind_check(host_domain_parsed.hostname or host_domain):
+                if not await _dns_rebind_check(host_domain_parsed.hostname or host_domain):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.py` around lines 166 - 171, The DNS resolution and sleep are blocking:
replace the synchronous socket.getaddrinfo and time.sleep calls with
non-blocking equivalents by running getaddrinfo in an executor (e.g.,
asyncio.get_running_loop().run_in_executor or asyncio.to_thread) and use
asyncio.sleep(0.2) instead of time.sleep; ensure the surrounding function is
async (or the call site invokes an async helper) and preserve the
socket.gaierror handling and the logic producing r1 and r2 so the rest of the
code that uses r1/r2 remains unchanged.

@comfyanonymous
Copy link
Copy Markdown
Member

Can you tell me which browser is vulnerable to this?

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.

create_origin_only_middleware is insufficient guard against DNS rebinding attacks

2 participants