fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
"""Pin the diffusion_server DNS-rebinding + wildcard-CORS regression.
|
|
|
|
|
|
|
|
|
|
Background: scripts/diffusion_server.py used to ship `allow_origins=["*"]`
|
|
|
|
|
with the default `--host=127.0.0.1` bind. Combined, that left the OpenAI-
|
|
|
|
|
compatible image API reachable from any browser tab via DNS-rebinding: an
|
|
|
|
|
attacker page resolves its own domain to 127.0.0.1 mid-fetch, the browser
|
|
|
|
|
forwards the request to the loopback server, and the wildcard CORS reply
|
|
|
|
|
lets the attacker page read the result + drive the GPU.
|
|
|
|
|
|
|
|
|
|
The fix narrows CORS to default-deny and adds a TrustedHostMiddleware
|
|
|
|
|
Host-header allowlist as a positive defense. These tests pin the allowlist
|
|
|
|
|
helpers + Starlette's middleware behavior so a future change can't silently
|
|
|
|
|
re-open the hole.
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
The tests AST-extract the security helpers — including the real
|
|
|
|
|
``_configure_security_middleware`` wiring — from diffusion_server.py and run
|
|
|
|
|
them against a fresh FastAPI app. That keeps the tests out of the torch /
|
|
|
|
|
diffusers import path while still exercising the production middleware wiring
|
|
|
|
|
instead of a hand-rebuilt copy.
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
"""
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
import ast
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
import importlib.util
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "diffusion_server.py"
|
|
|
|
|
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
_EXPECTED_NAMES = (
|
|
|
|
|
"_DEFAULT_ALLOWED_HOSTS",
|
|
|
|
|
"_DEFAULT_CORS_ORIGINS",
|
|
|
|
|
"_compute_allowed_hosts",
|
|
|
|
|
"_compute_cors_origins",
|
|
|
|
|
"_configure_security_middleware",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
def _load_helpers():
|
2026-06-07 22:42:11 +01:00
|
|
|
"""Extract the security helpers from diffusion_server.py via AST so the
|
|
|
|
|
tests exercise the production wiring without importing the module (which
|
|
|
|
|
would pull in torch / diffusers). Only the named top-level definitions are
|
|
|
|
|
compiled into a fresh module; everything else — including the heavy
|
|
|
|
|
imports — is left out. A renamed or removed helper fails loudly here."""
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
|
|
|
|
|
|
|
|
tree = ast.parse(_SCRIPT.read_text(encoding="utf-8"))
|
|
|
|
|
wanted: dict = {}
|
|
|
|
|
for node in tree.body:
|
|
|
|
|
if isinstance(node, ast.FunctionDef) and node.name in _EXPECTED_NAMES:
|
|
|
|
|
wanted[node.name] = node
|
|
|
|
|
elif isinstance(node, ast.Assign):
|
|
|
|
|
for target in node.targets:
|
|
|
|
|
if isinstance(target, ast.Name) and target.id in _EXPECTED_NAMES:
|
|
|
|
|
wanted[target.id] = node
|
|
|
|
|
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
|
|
|
if node.target.id in _EXPECTED_NAMES:
|
|
|
|
|
wanted[node.target.id] = node
|
|
|
|
|
|
|
|
|
|
missing = [name for name in _EXPECTED_NAMES if name not in wanted]
|
|
|
|
|
assert not missing, f"diffusion_server.py is missing expected helpers: {missing}"
|
|
|
|
|
|
|
|
|
|
module = ast.Module(body=[wanted[name] for name in _EXPECTED_NAMES], type_ignores=[])
|
|
|
|
|
ast.fix_missing_locations(module)
|
|
|
|
|
ns: dict = {
|
|
|
|
|
"TrustedHostMiddleware": TrustedHostMiddleware,
|
|
|
|
|
"CORSMiddleware": CORSMiddleware,
|
|
|
|
|
"RuntimeError": RuntimeError,
|
|
|
|
|
"list": list,
|
|
|
|
|
}
|
|
|
|
|
exec(compile(module, str(_SCRIPT), "exec"), ns)
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
return ns
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_compute_allowed_hosts_includes_loopback_and_bind_host():
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_allowed_hosts"]("0.0.0.0")
|
|
|
|
|
assert "0.0.0.0" in out
|
|
|
|
|
assert "127.0.0.1" in out
|
|
|
|
|
assert "localhost" in out
|
|
|
|
|
assert "::1" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_compute_allowed_hosts_dedupes_and_strips():
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
# Bind host duplicates a default + an extra duplicates a default + blanks
|
|
|
|
|
# all collapse into one entry per unique value, preserving stable order.
|
|
|
|
|
out = ns["_compute_allowed_hosts"]("127.0.0.1", extras=["localhost", "", " ", "lan.example"])
|
|
|
|
|
assert out == ["127.0.0.1", "localhost", "::1", "lan.example"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_compute_allowed_hosts_does_not_add_wildcard():
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_allowed_hosts"]("127.0.0.1")
|
|
|
|
|
assert "*" not in out, "wildcard host would re-open the DNS-rebinding hole"
|
|
|
|
|
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
def test_compute_allowed_hosts_preserves_explicit_wildcard():
|
|
|
|
|
# Behavior preservation: a wildcard is not added by default, but an
|
|
|
|
|
# operator who explicitly passes one is taken at their word (deduped,
|
|
|
|
|
# stripped, stable order). This pins current behavior, not policy.
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_allowed_hosts"]("127.0.0.1", extras=["*", " lan.example ", "*"])
|
|
|
|
|
assert out == ["127.0.0.1", "localhost", "::1", "*", "lan.example"]
|
|
|
|
|
|
|
|
|
|
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
def test_compute_cors_origins_default_deny():
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_cors_origins"]()
|
|
|
|
|
assert out == [], "default CORS allowlist must be empty (no cross-origin)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_compute_cors_origins_does_not_default_to_wildcard():
|
|
|
|
|
"""Regression: the original code shipped allow_origins=['*']. The fix
|
|
|
|
|
must NOT bring that back even when the operator passes nothing."""
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_cors_origins"](extras=None)
|
|
|
|
|
assert "*" not in out
|
|
|
|
|
out2 = ns["_compute_cors_origins"](extras=[])
|
|
|
|
|
assert "*" not in out2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_compute_cors_origins_honours_explicit_extras():
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_cors_origins"](extras=["http://localhost:7000", "", "http://localhost:7000"])
|
|
|
|
|
assert out == ["http://localhost:7000"]
|
|
|
|
|
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
def test_compute_cors_origins_preserves_explicit_wildcard():
|
|
|
|
|
# Behavior preservation: a wildcard is not the default, but an operator
|
|
|
|
|
# who explicitly passes one is taken at their word (deduped, stripped,
|
|
|
|
|
# stable order). This pins current behavior, not policy.
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
out = ns["_compute_cors_origins"](extras=["*", " http://localhost:7000 ", "*"])
|
|
|
|
|
assert out == ["*", "http://localhost:7000"]
|
|
|
|
|
|
|
|
|
|
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
# ── Live middleware integration: TrustedHostMiddleware + CORSMiddleware ─────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _starlette_available() -> bool:
|
|
|
|
|
return importlib.util.find_spec("starlette") is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _asgi_get(app, url, headers=None):
|
|
|
|
|
"""Drive a single GET against an ASGI ``app`` over httpx's in-process
|
|
|
|
|
``ASGITransport`` on a fresh event loop.
|
|
|
|
|
|
|
|
|
|
This deliberately avoids ``starlette.testclient.TestClient``: its
|
|
|
|
|
context-manager form spins up an ``anyio`` blocking portal (to run the
|
|
|
|
|
lifespan), which deadlocks under some pytest / anyio / asyncio test
|
|
|
|
|
configurations — the focused Host-header test hung indefinitely during
|
|
|
|
|
review (see PR #347). A direct ASGI call needs neither a portal nor a
|
|
|
|
|
lifespan, so it stays reliable regardless of the host project's async
|
|
|
|
|
test plugins.
|
|
|
|
|
|
|
|
|
|
The request ``Host`` is derived from ``url`` so the TrustedHost allowlist
|
|
|
|
|
sees exactly the hostname under test; ``Origin`` and friends go through
|
|
|
|
|
``headers``.
|
|
|
|
|
"""
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
async def _run():
|
|
|
|
|
transport = httpx.ASGITransport(app=app)
|
|
|
|
|
async with httpx.AsyncClient(transport=transport) as client:
|
|
|
|
|
return await client.get(url, headers=headers or {})
|
|
|
|
|
|
|
|
|
|
return asyncio.run(_run())
|
|
|
|
|
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
def _configured_app(ns, allowed_origins, route_called=None):
|
|
|
|
|
"""Fresh FastAPI app wired by the production `_configure_security_middleware`
|
|
|
|
|
with a loopback Host allowlist, plus a minimal route so accepted requests
|
|
|
|
|
can assert 200. If `route_called` is given, the route sets
|
|
|
|
|
``route_called["hit"] = True`` so callers can prove whether the inner app
|
|
|
|
|
was reached."""
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
from fastapi import FastAPI
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
2026-06-07 22:42:11 +01:00
|
|
|
ns["_configure_security_middleware"](
|
|
|
|
|
app, ns["_compute_allowed_hosts"]("127.0.0.1"), allowed_origins
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
def root():
|
|
|
|
|
if route_called is not None:
|
|
|
|
|
route_called["hit"] = True
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
return app
|
|
|
|
|
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
@pytest.mark.skipif(not _starlette_available(), reason="starlette not installed")
|
|
|
|
|
def test_trusted_host_middleware_rejects_attacker_host():
|
|
|
|
|
"""A request with an attacker-controlled Host header (the DNS-rebinding
|
|
|
|
|
surface) must be rejected by the production wiring before any route runs."""
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
route_called = {"hit": False}
|
|
|
|
|
app = _configured_app(ns, [], route_called=route_called)
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
# Legitimate request (Host: 127.0.0.1) reaches the route.
|
|
|
|
|
ok = _asgi_get(app, "http://127.0.0.1/")
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
assert ok.status_code == 200
|
2026-06-07 22:42:11 +01:00
|
|
|
assert route_called["hit"] is True
|
|
|
|
|
# Attacker-controlled hostname (DNS-rebinding scenario) is rejected before
|
|
|
|
|
# the route runs.
|
|
|
|
|
route_called["hit"] = False
|
|
|
|
|
bad = _asgi_get(app, "http://evil.example.com/")
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
assert bad.status_code == 400
|
2026-06-07 22:42:11 +01:00
|
|
|
assert route_called["hit"] is False
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not _starlette_available(), reason="starlette not installed")
|
|
|
|
|
def test_cors_default_deny_does_not_emit_wildcard_acao():
|
2026-06-07 22:42:11 +01:00
|
|
|
"""Default-deny CORS (no --allowed-origin) must not advertise any
|
|
|
|
|
Access-Control-Allow-Origin, so a browser blocks cross-origin readers."""
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
ns = _load_helpers()
|
|
|
|
|
cors_origins = ns["_compute_cors_origins"]()
|
|
|
|
|
assert cors_origins == []
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
app = _configured_app(ns, cors_origins)
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
|
|
|
|
|
# Host is allowed, so the request itself succeeds — but the response must
|
|
|
|
|
# carry no ACAO, so a real browser would block the attacker page from
|
|
|
|
|
# reading the body.
|
|
|
|
|
resp = _asgi_get(
|
2026-06-07 22:42:11 +01:00
|
|
|
app, "http://127.0.0.1/", headers={"Origin": "https://evil.example.com"}
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
)
|
2026-06-07 22:42:11 +01:00
|
|
|
assert resp.status_code == 200
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
acao = resp.headers.get("access-control-allow-origin")
|
|
|
|
|
assert acao is None or acao == "", (
|
|
|
|
|
f"unexpected ACAO header: {acao!r} — the regression was wildcard CORS, "
|
|
|
|
|
f"so any non-empty default fails this gate"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not _starlette_available(), reason="starlette not installed")
|
|
|
|
|
def test_explicit_cors_origin_does_not_widen_to_wildcard():
|
|
|
|
|
"""Even when the operator opts in to one cross-origin, that single origin
|
|
|
|
|
must not unlock a wildcard reflection for other origins."""
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
cors_origins = ns["_compute_cors_origins"](extras=["http://localhost:7000"])
|
|
|
|
|
|
2026-06-07 22:42:11 +01:00
|
|
|
app = _configured_app(ns, cors_origins)
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
|
|
|
|
|
# Allowed origin: ACAO echoes that origin (NOT '*').
|
|
|
|
|
ok = _asgi_get(
|
2026-06-07 22:42:11 +01:00
|
|
|
app, "http://127.0.0.1/", headers={"Origin": "http://localhost:7000"}
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
)
|
|
|
|
|
assert ok.status_code == 200
|
|
|
|
|
assert ok.headers.get("access-control-allow-origin") == "http://localhost:7000"
|
|
|
|
|
# Foreign origin: ACAO must NOT echo it, must NOT be '*'.
|
|
|
|
|
bad = _asgi_get(
|
2026-06-07 22:42:11 +01:00
|
|
|
app, "http://127.0.0.1/", headers={"Origin": "https://evil.example.com"}
|
fix(security): close DNS-rebinding hole on diffusion_server (wildcard CORS + missing Host check) (#347)
* fix(security): close DNS-rebinding hole on diffusion_server
scripts/diffusion_server.py used to ship `allow_origins=["*"]` with the
default `--host=127.0.0.1` bind. Combined, that left the OpenAI-compatible
image API reachable from any browser tab via DNS-rebinding: an attacker page
resolves its own domain to 127.0.0.1 mid-fetch, the browser forwards the
request to the loopback server, the server processes it (no Host check), and
the wildcard CORS reply lets the attacker page read the result + drive the
GPU. CWE-346 + CWE-942 + CWE-352 (DNS-rebinding bridge).
Fix:
- Drop the wildcard CORS at module load (default-deny).
- Install `TrustedHostMiddleware` with a loopback allowlist so DNS-rebound
requests are rejected by the middleware before any route runs.
- Add additive `--allowed-host` / `--allowed-origin` CLI flags so operators
who need browser access on a specific origin can opt in explicitly without
re-introducing the wildcard.
Tests: tests/test_diffusion_server_security.py (9 cases) pin the allowlist
helpers, the default-deny CORS behavior, and the live middleware paths via
Starlette's TestClient.
Detected by Aeon + semgrep + manual review.
Severity: medium.
CWE-346 / CWE-942 / CWE-352.
* test(diffusion-server): drive ASGI app via httpx, not TestClient portal
The TrustedHost/CORS integration tests used `with TestClient(app) as
client:`, whose context-manager form spins up an anyio blocking portal to
run the app lifespan. Under the repo's pytest setup (anyio plugin active, a
stray asyncio_mode option, no pytest-asyncio) that portal deadlocks —
`test_trusted_host_middleware_rejects_attacker_host` hung indefinitely in
review before emitting any assertion output.
Replace the TestClient usage with a tiny _asgi_get() helper that drives the
ASGI app over httpx.ASGITransport on a fresh event loop (asyncio.run). No
portal, no lifespan, no dependency on the host project's async test plugins.
Host is taken from the request URL so TrustedHostMiddleware sees the exact
hostname under test; Origin goes through headers. Assertions are unchanged.
Focused test now passes in 0.12s; full file 9 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: aeonframework <aeonframework@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:34:39 -04:00
|
|
|
)
|
|
|
|
|
bad_acao = bad.headers.get("access-control-allow-origin")
|
|
|
|
|
assert bad_acao != "*"
|
|
|
|
|
assert bad_acao != "https://evil.example.com"
|
2026-06-07 22:42:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not _starlette_available(), reason="starlette not installed")
|
|
|
|
|
def test_configure_security_middleware_preserves_order():
|
|
|
|
|
"""CORS is added last so it wraps TrustedHost (outermost). The production
|
|
|
|
|
order must be user_middleware == [CORSMiddleware, TrustedHostMiddleware];
|
|
|
|
|
default-deny installs the Host allowlist alone."""
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
|
|
|
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
|
|
|
|
|
with_cors = _configured_app(ns, ns["_compute_cors_origins"](extras=["http://localhost:7000"]))
|
|
|
|
|
assert [m.cls for m in with_cors.user_middleware] == [CORSMiddleware, TrustedHostMiddleware]
|
|
|
|
|
|
|
|
|
|
default_deny = _configured_app(ns, [])
|
|
|
|
|
assert [m.cls for m in default_deny.user_middleware] == [TrustedHostMiddleware]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not _starlette_available(), reason="starlette not installed")
|
|
|
|
|
def test_configure_security_middleware_is_idempotent_before_serving():
|
|
|
|
|
"""Re-running configuration (module-load defaults, then CLI override)
|
|
|
|
|
replaces the stack rather than accumulating duplicate middleware."""
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
|
|
|
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
allowed = ns["_compute_allowed_hosts"]("127.0.0.1")
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
ns["_configure_security_middleware"](app, allowed, [])
|
|
|
|
|
ns["_configure_security_middleware"](
|
|
|
|
|
app, allowed, ns["_compute_cors_origins"](extras=["http://localhost:7000"])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
classes = [m.cls for m in app.user_middleware]
|
|
|
|
|
assert classes == [CORSMiddleware, TrustedHostMiddleware]
|
|
|
|
|
assert classes.count(TrustedHostMiddleware) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not _starlette_available(), reason="starlette not installed")
|
|
|
|
|
def test_configure_security_middleware_rejects_late_call():
|
|
|
|
|
"""Once the middleware stack is built, the helper must raise before
|
|
|
|
|
mutating user_middleware so a late reconfigure can't silently no-op."""
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
|
|
|
|
|
ns = _load_helpers()
|
|
|
|
|
allowed = ns["_compute_allowed_hosts"]("127.0.0.1")
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
ns["_configure_security_middleware"](app, allowed, [])
|
|
|
|
|
before = list(app.user_middleware)
|
|
|
|
|
|
|
|
|
|
# Simulate the app having started serving (stack built lazily on first req).
|
|
|
|
|
app.middleware_stack = app.build_middleware_stack()
|
|
|
|
|
assert app.middleware_stack is not None
|
|
|
|
|
|
|
|
|
|
with pytest.raises(RuntimeError):
|
|
|
|
|
ns["_configure_security_middleware"](app, ["lan.example"], [])
|
|
|
|
|
# Guard fired before mutating: user_middleware is untouched.
|
|
|
|
|
assert list(app.user_middleware) == before
|