Skip to content

fix(security): uvicorn-CLI bind-parity shim so the SEC-F22 guard sees the real --host (SEC-F27)#434

Merged
pcalnon merged 1 commit into
mainfrom
fix/canopy-uvicorn-cli-bind-parity-shim
Jul 7, 2026
Merged

fix(security): uvicorn-CLI bind-parity shim so the SEC-F22 guard sees the real --host (SEC-F27)#434
pcalnon merged 1 commit into
mainfrom
fix/canopy-uvicorn-cli-bind-parity-shim

Conversation

@pcalnon

@pcalnon pcalnon commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Launch-path bind audit SEC-F27 (reclassified). canopy's lifespan calls enforce_loopback_bind_guard(settings.server.host, ...), but a uvicorn main:app --host X launch binds X without ever setting JUNIPER_CANOPY_SERVER__HOST — so the guard read the loopback default and passed while uvicorn bound a public socket. That was the undefended root cause of SEC-F23/F24. juniper-cascor already closes this with _settings_with_uvicorn_cli_bind; canopy had no equivalent — this adds it.

What changed

  • src/security.py: new settings_with_uvicorn_cli_bind(settings, argv=None) (+ _cli_option_value helper), mirroring cascor's shim adapted to canopy's nested settings.server.host/port and main:app entrypoint. Overlays a uvicorn CLI --host/--port onto a transient settings copy so the guard evaluates the real bind.
  • src/main.py: apply it right after get_settings(), so every consumer (guard, startup logs, app.state) sees the real bind. A python main.py launch carries no uvicorn CLI args → no-op.

This is the generic defense: it closes the bypass class (any uvicorn main:app --host X), not just the three launchers fixed in #433.

Why loopback-default launchers and the shim

#433 makes the shipped launchers bind loopback by default (honest posture). This shim is defense-in-depth: even if a future launcher passes --host 0.0.0.0, the guard now evaluates it (and hard-fails without attestation) instead of being blind.

Testing

  • 7 new cases in TestUvicornCliBindParity: no-op on python main.py; --host overlay in 3 argv forms (--host X, --host=X, python -m uvicorn); --port overlay; non-integer --port ignored; and the end-to-end guard-refuses-on-CLI-host case (the real point).
  • Full test_bind_guard.py suite green in JuniperCanopy1; pre-commit (black/isort/flake8/mypy/bandit) clean on all three files.

Note

The cascor docstring at src/api/app.py:100 is not changed — it correctly documents cascor's own defense. See the SEC-F27 reclassification in juniper-ml PR #630 §4/§7.

See juniper-ml notes/JUNIPER_2026-07-06_JUNIPER-ECOSYSTEM_LAUNCH-PATH-BIND-AUDIT.md (SEC-F27).

🤖 Generated with Claude Code

https://claude.ai/code/session_01LX5ToumBaABH3QutQC86sM

…sees the real --host (SEC-F27)

canopy's lifespan calls enforce_loopback_bind_guard(settings.server.host), but a
`uvicorn main:app --host X` launch binds X without ever setting
JUNIPER_CANOPY_SERVER__HOST, so the guard read the loopback default and passed
while uvicorn bound a public socket -- an undefended bypass (the root cause of
SEC-F23/F24). juniper-cascor already closes this via _settings_with_uvicorn_cli_bind;
canopy had no equivalent.

Add settings_with_uvicorn_cli_bind() to security.py (with _cli_option_value), mirror
cascor's shim adapted to canopy's nested settings.server + main:app entrypoint, and
apply it in main.py right after get_settings() so every consumer (guard, logs,
app.state) sees the real bind. A `python main.py` launch has no uvicorn CLI args, so
it is a no-op there. Generic defense: closes the bypass class, not just the three
launchers fixed in the loopback-default PR.

Tests: 7 new cases in TestUvicornCliBindParity (no-op path, --host/--port overlay in
3 argv forms, non-integer --port ignored, and end-to-end guard-refuses-on-CLI-host).
Full bind-guard suite green; black/isort/flake8/mypy/bandit clean.

See juniper-ml notes/JUNIPER_2026-07-06_JUNIPER-ECOSYSTEM_LAUNCH-PATH-BIND-AUDIT.md (SEC-F27).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LX5ToumBaABH3QutQC86sM
@pcalnon pcalnon self-assigned this Jul 7, 2026

@pcalnon pcalnon left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

approved

@pcalnon pcalnon merged commit ecc67d4 into main Jul 7, 2026
39 of 40 checks passed
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.

1 participant