[P0][SECURITY] BUG-41: shipped brain binds ALL interfaces with no auth — the launcher's loopback-lock and auth key are inert (EL_HTTP_BIND_HOST / EL_HTTP_AUTH_KEY exist nowhere in the engine) #110

Open
opened 2026-08-06 03:24:24 +00:00 by tim.lingo · 1 comment
Member

BUG-41. Filed by Neuron (Tim's instance) 2026-08-05. Every fact below was measured first-hand for this filing unless explicitly marked otherwise.

What is wrong

The macOS launcher believes it starts the brain loopback-locked and authenticated. It does not. The two environment variables it sets to achieve that — EL_HTTP_BIND_HOST and EL_HTTP_AUTH_KEYdo not exist anywhere in the engine. Nothing reads them. The brain binds all interfaces and serves every request unauthenticated, while the launcher logs loopback-locked.

In plain terms: on shared wifi — a coffee shop, an office, a hotel, a conference — anything else on that network can read and write the user's memory. No password, no token, no prompt.

Evidence

1. The launcher's intent (and its false log line)

installer/macos/neuron-daemons.sh — round-7 copy at _wt-beta-round7/resources/macos-arm64/neuron-daemons.sh. It mints a per-install secret and passes both knobs:

232: SOUL_AUTH_KEY=$(/usr/bin/security find-generic-password -s "ai.neuron.soul-auth" -a "key" -w 2>/dev/null || true)
233: if [ -z "${SOUL_AUTH_KEY:-}" ]; then
234:     SOUL_AUTH_KEY=$(/usr/bin/openssl rand -base64 48 | tr -d '\n')
...
241: log "lifecycle: starting soul on 127.0.0.1:${SOUL_PORT} (local mode, loopback-locked) provider=${PROVIDER} ..."
...
290:     EL_HTTP_BIND_HOST="127.0.0.1" \
291:     EL_HTTP_AUTH_KEY="$SOUL_AUTH_KEY" \

2. Raw byte search of the shipped brain

Binary: _wt-beta-round7/resources/macos-arm64/neuron — this is SOUL_BIN per neuron-daemons.sh:31. Mach-O 64-bit arm64, 899,424 bytes, md5 85a19bd6e83a9f96f60a7b7a17e5745d, mtime 2026-08-05 20:45.

strings -a <bin> | grep -cx <name>:

env var name occurrences in the shipped brain
EL_HTTP_TIMEOUT_MS 1
NEURON_PORT 1
NEURON_LLM_0_URL 1
EL_HTTP_AUTH_KEY 0
EL_HTTP_BIND_HOST 0
AUTH_KEY 0
BIND_HOST 0

EL_HTTP_TIMEOUT_MS and NEURON_PORT are present, which proves the technique finds env-var names the binary genuinely reads. The auth and bind knobs are simply not in there.

3. Source confirms this is not a packaging accident

A tree-wide search of every *.el, *.elh, *.c and *.h — including the vendored runtime vendor/el-runtime/v1.0.0-20260501/el_runtime.c — returns zero hits for EL_HTTP_BIND_HOST and EL_HTTP_AUTH_KEY. The only knob of that family that exists is EL_HTTP_TIMEOUT_MS (el_runtime.c:780, default 60000). These two were never implemented.

4. Live on this machine, 2026-08-05 22:23:42 CDT

$ lsof -nP -iTCP:7770 -iTCP:7779 -iTCP:17779 -sTCP:LISTEN
COMMAND     PID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
neuron-mc  1149 timlingo    3u  IPv6 0x509af6dd3bc29f43      0t0  TCP *:17779 (LISTEN)
neuron-mc  5927 timlingo    3u  IPv6 0xf37c5dc5530893c1      0t0  TCP *:7779 (LISTEN)
soul      95688 timlingo    3u  IPv6 0x7552658848061e9e      0t0  TCP *:7770 (LISTEN)

*:7770, not 127.0.0.1:7770. The MCP wrapper and proxy are wide open too.

5. Exploited — measured earlier today, NOT re-run for this filing

Marked separately because it is the one item here I did not personally re-execute (the filing pass was read-only on the live daemons): a plain HTTP GET issued from the machine's own LAN address returned 200, and an unauthenticated POST from the LAN wrote a memory and came back {"ok":true}. Read and write, from off-box, with no credential.

This is pre-existing, not a round-7 regression

The round-6 and round-7 brains are byte-identical — both md5 85a19bd6e83a9f96f60a7b7a17e5745d, both 899,424 bytes. Every DMG built so far ships this.

What the fix needs (engine-side, yours)

  1. Implement EL_HTTP_BIND_HOST in the runtime listener, and make the default 127.0.0.1 — safe when unset, not merely safe when the launcher remembers to set it.
  2. Implement EL_HTTP_AUTH_KEY as a required shared-secret check on every route whenever it is set. The launcher already generates and stores the secret in the Keychain, so the app side is done the moment the engine honors it.
  3. Make the loopback-locked log line conditional on the bind actually having succeeded. A log line that asserts a security property it never verified is how this stayed invisible.

Until (1) and (2) exist in the binary, no launcher-side change can fix this — the app cannot lock a door the engine does not have. That is why this is filed engine-side rather than as an app bug.

Suggested gate

Worth a CI assertion once implemented: boot the built brain and fail the build if lsof shows anything other than a loopback bind, and if an unauthenticated request succeeds. This class of defect is invisible to every test that runs on localhost.

Cross-refs: neuron#62 (make inference endpoint + auth token configurable) touches the same auth surface. neuron-ui#213 is the 2026-08-03 bug harvest this continues.

**BUG-41. Filed by Neuron (Tim's instance) 2026-08-05. Every fact below was measured first-hand for this filing unless explicitly marked otherwise.** ## What is wrong The macOS launcher believes it starts the brain loopback-locked and authenticated. It does not. The two environment variables it sets to achieve that — `EL_HTTP_BIND_HOST` and `EL_HTTP_AUTH_KEY` — **do not exist anywhere in the engine**. Nothing reads them. The brain binds all interfaces and serves every request unauthenticated, while the launcher logs `loopback-locked`. In plain terms: on shared wifi — a coffee shop, an office, a hotel, a conference — anything else on that network can read and write the user's memory. No password, no token, no prompt. ## Evidence ### 1. The launcher's intent (and its false log line) `installer/macos/neuron-daemons.sh` — round-7 copy at `_wt-beta-round7/resources/macos-arm64/neuron-daemons.sh`. It mints a per-install secret and passes both knobs: ``` 232: SOUL_AUTH_KEY=$(/usr/bin/security find-generic-password -s "ai.neuron.soul-auth" -a "key" -w 2>/dev/null || true) 233: if [ -z "${SOUL_AUTH_KEY:-}" ]; then 234: SOUL_AUTH_KEY=$(/usr/bin/openssl rand -base64 48 | tr -d '\n') ... 241: log "lifecycle: starting soul on 127.0.0.1:${SOUL_PORT} (local mode, loopback-locked) provider=${PROVIDER} ..." ... 290: EL_HTTP_BIND_HOST="127.0.0.1" \ 291: EL_HTTP_AUTH_KEY="$SOUL_AUTH_KEY" \ ``` ### 2. Raw byte search of the shipped brain Binary: `_wt-beta-round7/resources/macos-arm64/neuron` — this is `SOUL_BIN` per `neuron-daemons.sh:31`. Mach-O 64-bit arm64, 899,424 bytes, md5 `85a19bd6e83a9f96f60a7b7a17e5745d`, mtime 2026-08-05 20:45. `strings -a <bin> | grep -cx <name>`: | env var name | occurrences in the shipped brain | |---|---| | `EL_HTTP_TIMEOUT_MS` | **1** | | `NEURON_PORT` | 1 | | `NEURON_LLM_0_URL` | 1 | | `EL_HTTP_AUTH_KEY` | **0** | | `EL_HTTP_BIND_HOST` | **0** | | `AUTH_KEY` | **0** | | `BIND_HOST` | **0** | `EL_HTTP_TIMEOUT_MS` and `NEURON_PORT` are present, which proves the technique finds env-var names the binary genuinely reads. The auth and bind knobs are simply not in there. ### 3. Source confirms this is not a packaging accident A tree-wide search of every `*.el`, `*.elh`, `*.c` and `*.h` — including the vendored runtime `vendor/el-runtime/v1.0.0-20260501/el_runtime.c` — returns **zero** hits for `EL_HTTP_BIND_HOST` and `EL_HTTP_AUTH_KEY`. The only knob of that family that exists is `EL_HTTP_TIMEOUT_MS` (`el_runtime.c:780`, default 60000). These two were never implemented. ### 4. Live on this machine, 2026-08-05 22:23:42 CDT ``` $ lsof -nP -iTCP:7770 -iTCP:7779 -iTCP:17779 -sTCP:LISTEN COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME neuron-mc 1149 timlingo 3u IPv6 0x509af6dd3bc29f43 0t0 TCP *:17779 (LISTEN) neuron-mc 5927 timlingo 3u IPv6 0xf37c5dc5530893c1 0t0 TCP *:7779 (LISTEN) soul 95688 timlingo 3u IPv6 0x7552658848061e9e 0t0 TCP *:7770 (LISTEN) ``` `*:7770`, not `127.0.0.1:7770`. The MCP wrapper and proxy are wide open too. ### 5. Exploited — measured earlier today, NOT re-run for this filing Marked separately because it is the one item here I did not personally re-execute (the filing pass was read-only on the live daemons): a plain HTTP GET issued from the machine's own LAN address returned **200**, and an **unauthenticated POST from the LAN wrote a memory and came back `{"ok":true}`**. Read and write, from off-box, with no credential. ## This is pre-existing, not a round-7 regression The round-6 and round-7 brains are byte-identical — both md5 `85a19bd6e83a9f96f60a7b7a17e5745d`, both 899,424 bytes. Every DMG built so far ships this. ## What the fix needs (engine-side, yours) 1. **Implement `EL_HTTP_BIND_HOST`** in the runtime listener, and make the *default* `127.0.0.1` — safe when unset, not merely safe when the launcher remembers to set it. 2. **Implement `EL_HTTP_AUTH_KEY`** as a required shared-secret check on every route whenever it is set. The launcher already generates and stores the secret in the Keychain, so the app side is done the moment the engine honors it. 3. **Make the `loopback-locked` log line conditional** on the bind actually having succeeded. A log line that asserts a security property it never verified is how this stayed invisible. Until (1) and (2) exist in the binary, **no launcher-side change can fix this** — the app cannot lock a door the engine does not have. That is why this is filed engine-side rather than as an app bug. ## Suggested gate Worth a CI assertion once implemented: boot the built brain and fail the build if `lsof` shows anything other than a loopback bind, and if an unauthenticated request succeeds. This class of defect is invisible to every test that runs on localhost. Cross-refs: neuron#62 (make inference endpoint + auth token configurable) touches the same auth surface. neuron-ui#213 is the 2026-08-03 bug harvest this continues.
tim.lingo added the P0securityblocks-public-betaBETA-CRITICAL labels 2026-08-06 03:24:25 +00:00
Author
Member

Independent corroboration from the soul's own log

2026-08-05. The brain announces the wide bind itself. From ~/neuron-dev-stack/logs/dev-soul.err.log on the live instance:

[http] async listening on [::]:7770 (dual-stack)

[::] is the IPv6 any-address, dual-stack — every interface, which is what the lsof line in the report shows as TCP *:7770 (LISTEN). So this is not an artifact of how lsof renders a socket: the HTTP layer is being told to listen on the any-address and says so.

This also confirms there is no code path consulting a bind-host preference at listener setup — consistent with the byte search finding zero occurrences of EL_HTTP_BIND_HOST in both the shipped brain and the entire source tree.

## Independent corroboration from the soul's own log 2026-08-05. The brain announces the wide bind itself. From `~/neuron-dev-stack/logs/dev-soul.err.log` on the live instance: ``` [http] async listening on [::]:7770 (dual-stack) ``` `[::]` is the IPv6 any-address, dual-stack — every interface, which is what the `lsof` line in the report shows as `TCP *:7770 (LISTEN)`. So this is not an artifact of how `lsof` renders a socket: the HTTP layer is being told to listen on the any-address and says so. This also confirms there is no code path consulting a bind-host preference at listener setup — consistent with the byte search finding zero occurrences of `EL_HTTP_BIND_HOST` in both the shipped brain and the entire source tree.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: neuron-technologies/neuron#110