Skip to content

fix(trace): bind OTLP/gRPC listener to 127.0.0.1 by default#943

Open
sebastiondev wants to merge 2 commits into
katanemo:mainfrom
sebastiondev:fix/cwe287-trace-cmd-grpc-e334
Open

fix(trace): bind OTLP/gRPC listener to 127.0.0.1 by default#943
sebastiondev wants to merge 2 commits into
katanemo:mainfrom
sebastiondev:fix/cwe287-trace-cmd-grpc-e334

Conversation

@sebastiondev
Copy link
Copy Markdown

Summary

Bind the OTLP/gRPC trace listener to 127.0.0.1 by default instead of 0.0.0.0, so a local development tool doesn't unintentionally expose an unauthenticated trace receiver to the entire local network.

The issue

planoai trace listen and the in-process collector started by planoai up create a gRPC server via server.add_insecure_port(...) (see cli/planoai/trace_cmd.py). The server:

  • has no authentication and no TLS
  • accepts OTLP spans from any client that can reach it
  • exposes a _get_traces JSON query method that returns all collected spans, including HTTP request headers captured by instrumented apps (which commonly contain Authorization, API keys, cookies, etc.)

Three places hard-coded the bind host to 0.0.0.0:

  • cli/planoai/trace_cmd.py — the trace listen CLI default
  • cli/planoai/trace_cmd.pystart_trace_listener_background() default (called from planoai up)
  • cli/planoai/main.py — the console message printed on startup

On a developer laptop on shared Wi‑Fi (coffee shop, conference, office LAN), any peer on the same network can:

  1. Exfiltrate traces by connecting to <dev-ip>:4317 and calling _get_traces, harvesting headers/tokens captured from the developer's own app traffic.
  2. Inject forged spans that the developer will then trust when debugging.

The only precondition is network reachability on the gRPC port — there is no existing auth or framework protection in front of this listener.

The fix

Change the default host from 0.0.0.0 to 127.0.0.1 in all three locations. The host parameter remains user-overridable for anyone who deliberately wants to receive traces from another machine. This matches the standard convention for developer-facing local servers (Jupyter, Flask debug, OpenTelemetry collectors, etc.).

 cli/planoai/main.py        | 2 +-
 cli/planoai/trace_cmd.py   | 4 ++--
 cli/test/test_trace_cmd.py | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

Tests

  • Updated test_trace_listen_starts_listener_with_defaults to assert the new 127.0.0.1 default.
  • The existing test suite for trace_cmd still passes — the host parameter is still threaded through unchanged, only the default value moves.

Adversarial review

Before submitting, I tried to disprove this. Could existing controls already block LAN access? No — add_insecure_port has no auth layer, there's no firewall assumption documented for planoai, and _get_traces returns full span data including captured HTTP headers. Could users who want LAN exposure be broken? The host argument is still respected on every code path, so explicitly passing 0.0.0.0 (or any interface) continues to work; only the unconfigured default changes. Other 0.0.0.0 occurrences in the repo (egress config in core.py, the obs collector, port-probe in main.py) are unrelated to this listener and deliberately left alone.

CWE-306 (Missing Authentication for Critical Function) / CWE-668 (Exposure of Resource to Wrong Sphere) on a developer-tooling listener. Severity is moderate — it requires local-network adjacency, but no user interaction beyond running the documented command, and the data exposed can include bearer tokens from the developer's own traffic.

Happy to adjust framing or add a --bind flag note in docs if useful.

Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

The OTLP/gRPC trace listener was binding to 0.0.0.0 by default, exposing
the unauthenticated trace service to the network. This allows any host on
the same network to inject fake spans or exfiltrate collected trace data
(which may contain sensitive attributes like API keys and HTTP headers).

Bind to 127.0.0.1 (localhost) by default so the trace listener is only
accessible from the local machine.

CWE-287
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