Skip to content

Hardening: safer CORS config + localhost bind defaults#2767

Merged
crivetimihai merged 3 commits intoIBM:mainfrom
TheodorNEngoy:codex/hardening-cors-bind
Feb 21, 2026
Merged

Hardening: safer CORS config + localhost bind defaults#2767
crivetimihai merged 3 commits intoIBM:mainfrom
TheodorNEngoy:codex/hardening-cors-bind

Conversation

@TheodorNEngoy
Copy link
Copy Markdown
Contributor

This hardens the LangChain agent runtime defaults to avoid two common security footguns for network-exposed MCP-adjacent servers:

  • CORS: stop combining CORS_ORIGINS=* with credentials. The app now reads CORS_ORIGINS/CORS_CREDENTIALS and forces credentials off when CORS_ORIGINS=* (with a warning), since wildcard+credentials is unsafe.
  • Bind host: default local dev runs to 127.0.0.1 instead of 0.0.0.0 in Makefile and start_agent.py (still configurable via HOST/PORT). The container Dockerfile remains --host 0.0.0.0.

Also updates .env.example to reflect safer defaults (CORS disabled by default; loopback bind by default).

Rationale: reduces accidental remote exposure + credentialed cross-origin access while keeping intentional remote deployments opt-in.

@TheodorNEngoy TheodorNEngoy force-pushed the codex/hardening-cors-bind branch from 722d4c0 to 2850c4a Compare February 8, 2026 18:36
@crivetimihai crivetimihai self-assigned this Feb 9, 2026
@crivetimihai crivetimihai added this to the Release 1.0.0-RC1 milestone Feb 9, 2026
@crivetimihai
Copy link
Copy Markdown
Member

Thanks for this hardening PR, @TheodorNEngoy! Solid security improvements:

  • Catching the wildcard CORS + credentials anti-pattern and logging a warning is exactly right.
  • Defaulting to localhost binding and CORS-disabled is good principle of least privilege.

A couple of minor suggestions:

  1. DRY the port parsing: _env_int is defined in app.py but the same logic is manually reimplemented in start_agent.py (lines 116-120). Consider sharing the helper.
  2. Makefile hardcodes host: The run/dev targets use literal --host 127.0.0.1, so HOST=0.0.0.0 make run won't work as expected. Using --host $${HOST:-127.0.0.1} would be more consistent.

Neither is a blocker. LGTM!

@TheodorNEngoy
Copy link
Copy Markdown
Contributor Author

Thanks! Addressed both nits:

  • DRY port parsing: factored env helpers into agent_runtimes/langchain_agent/env_utils.py and start_agent.py now uses shared _env_int().
  • Makefile HOST override: run/dev now use --host 65847{HOST:-127.0.0.1} and --port 65847{PORT:-8000} so HOST=0.0.0.0 make run behaves as expected.

crivetimihai
crivetimihai previously approved these changes Feb 19, 2026
Copy link
Copy Markdown
Member

@crivetimihai crivetimihai left a comment

Choose a reason for hiding this comment

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

Thanks @TheodorNEngoy — this is well-executed. Both suggestions from the previous review have been properly addressed:

  • DRY the port parsing — extracted _env_bool, _env_int, _parse_csv into env_utils.py. Clean.
  • Makefile host overriderun/dev targets now use $${HOST:-127.0.0.1}. Works correctly.

The CORS hardening is sound (empty default = disabled, wildcard+credentials guard with warning, defense-in-depth), and the localhost bind default is a good security improvement.

One edge case to consider: the wildcard-with-credentials guard only fires when CORS_ORIGINS is exactly "*", not when "*" appears among a comma-separated list like "*,http://example.com". While unlikely, checking the parsed list instead of the raw string would make the guard more robust:

cors_allow_origins = _parse_csv(cors_origins_raw)
if "*" in cors_allow_origins:
    cors_allow_origins = ["*"]
    if cors_allow_credentials:
        logger.warning("...")
        cors_allow_credentials = False

This is a minor suggestion — the PR is solid as-is. LGTM.

Theodor N. Engøy and others added 3 commits February 21, 2026 09:55
Signed-off-by: Theodor N. Engøy <theodornengoy@Mac.home>
Signed-off-by: Theodor N. Engøy <theodornengoy@eduroam-193-157-246-146.wlan.uio.no>
- Fix CORS wildcard bypass: check parsed origin list instead of raw
  string so '*,https://example.com' is caught
- Validate LOG_LEVEL against allowed uvicorn levels with fallback
- Add 44 differential tests for env_utils and CORS configuration
- Remove unused pytest import (Ruff F401)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Copy link
Copy Markdown
Member

@crivetimihai crivetimihai left a comment

Choose a reason for hiding this comment

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

Thanks @TheodorNEngoy — this is a solid security hardening contribution!

What was done

Rebased onto main (clean, no conflicts) and applied three fixes during review:

  1. CORS wildcard bypass fix (High): The original guard (cors_origins_raw == "*") only caught the exact * string. Mixed inputs like *,https://example.com bypassed the safety check while Starlette still treated the list as allow-all. Fixed by checking "*" in cors_allow_origins on the parsed list, and normalizing any wildcard-containing list to ["*"].

  2. LOG_LEVEL validation (Medium): LOG_LEVEL was passed to uvicorn without validation — invalid values like warn or " info " would crash at startup. Added strip/lowercase/validation against allowed uvicorn levels with a warning fallback.

  3. Unused import (Low): Removed unused pytest import in test file.

Tests added

44 differential tests covering all new code:

  • test_env_utils.py (32 tests): _env_bool, _env_int, _parse_csv — truthy/falsy/default/whitespace/edge cases
  • test_cors_config.py (12 tests): CORS disabled by default, explicit origins, wildcard safety, mixed wildcard bypass (the new regression tests), edge cases

Verification

All pass:

  • make doctest — 1193 passed
  • make test — 12447 passed
  • make pylint — 10.00/10
  • make flake8 — clean

@crivetimihai crivetimihai merged commit 1d0ed46 into IBM:main Feb 21, 2026
40 of 42 checks passed
vishu-bh pushed a commit that referenced this pull request Feb 24, 2026
* Hardening: safer CORS + localhost bind defaults

Signed-off-by: Theodor N. Engøy <theodornengoy@Mac.home>

* langchain agent: DRY env parsing + Makefile HOST override

Signed-off-by: Theodor N. Engøy <theodornengoy@eduroam-193-157-246-146.wlan.uio.no>

* fix(security): harden CORS wildcard guard, validate LOG_LEVEL, add tests

- Fix CORS wildcard bypass: check parsed origin list instead of raw
  string so '*,https://example.com' is caught
- Validate LOG_LEVEL against allowed uvicorn levels with fallback
- Add 44 differential tests for env_utils and CORS configuration
- Remove unused pytest import (Ruff F401)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Theodor N. Engøy <theodornengoy@Mac.home>
Signed-off-by: Theodor N. Engøy <theodornengoy@eduroam-193-157-246-146.wlan.uio.no>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Theodor N. Engøy <theodornengoy@Mac.home>
Co-authored-by: Theodor N. Engøy <theodornengoy@eduroam-193-157-246-146.wlan.uio.no>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants