Skip to content

Commit fff5dcc

Browse files
committed
feat(ui): add configurable host binding
Signed-off-by: Blamp26 <98893587+Blamp26@users.noreply.github.com>
1 parent 8c9ac98 commit fff5dcc

12 files changed

Lines changed: 259 additions & 45 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,19 @@ The UI ships as a separate `ui` build (it embeds the frontend). The default inst
114114
Then run it:
115115

116116
```bash
117-
codebase-memory-mcp --ui=true --port=9749
117+
codebase-memory-mcp --ui=true --host=127.0.0.1 --port=9749
118118
```
119119

120120
Open `http://localhost:9749` in your browser. The UI runs as a background thread alongside the MCP server — it's available whenever your agent is connected.
121121

122+
The default host is `127.0.0.1` and is persisted with the UI port. To share the UI on a trusted LAN, bind a specific local address:
123+
124+
```bash
125+
codebase-memory-mcp --ui=true --host=192.168.88.26 --port=9749
126+
```
127+
128+
Use `--host=0.0.0.0` only when you need all interfaces. The UI has no authentication; a non-loopback bind is reachable over the network and may expose codebase graph data and UI actions. Protect it with a firewall or trusted network, and prefer a specific LAN IP over `0.0.0.0`.
129+
122130
### Auto-Index
123131

124132
Enable automatic indexing on MCP session start:

docs/CONFIGURATION.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This page documents the configuration files that `codebase-memory-mcp` reads or
99
| Global custom extension mapping | `$XDG_CONFIG_HOME/codebase-memory-mcp/config.json` | JSON | Falls back to `~/.config/codebase-memory-mcp/config.json` when `XDG_CONFIG_HOME` is unset. |
1010
| Per-project custom extension mapping | `{repo_root}/.codebase-memory.json` | JSON | Overrides conflicting global `extra_extensions` entries. |
1111
| CLI-managed runtime settings | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/_config.db` | SQLite | Written by `codebase-memory-mcp config set/reset`. |
12-
| UI settings | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/config.json` | JSON | Stores `ui_enabled` and `ui_port`. |
12+
| UI settings | `${CBM_CACHE_DIR:-~/.cache/codebase-memory-mcp}/config.json` | JSON | Stores `ui_enabled`, `ui_port`, and numeric IPv4 `ui_host`. |
1313

1414
## 1. Custom File Extension Mapping
1515

@@ -95,13 +95,35 @@ Current format:
9595
```json
9696
{
9797
"ui_enabled": false,
98-
"ui_port": 9749
98+
"ui_port": 9749,
99+
"ui_host": "127.0.0.1"
99100
}
100101
```
101102

102103
Notes:
103104

104105
- If the UI-enabled binary has embedded assets and no UI config file exists yet, the UI auto-enables on first run.
106+
- `ui_host` defaults to `127.0.0.1`; config files written before this field existed remain localhost-only.
107+
- `--host=<IPv4>` accepts numeric IPv4 addresses only and persists the setting, just like `--port`.
108+
- Localhost-only UI:
109+
110+
```bash
111+
codebase-memory-mcp --ui=true --host=127.0.0.1 --port=9749
112+
```
113+
114+
- Specific LAN interface (recommended when sharing on a trusted network):
115+
116+
```bash
117+
codebase-memory-mcp --ui=true --host=192.168.88.26 --port=9749
118+
```
119+
120+
- All interfaces:
121+
122+
```bash
123+
codebase-memory-mcp --ui=true --host=0.0.0.0 --port=9749
124+
```
125+
126+
Binding to a non-loopback address prints a warning because the UI has no authentication, is reachable over the network, and may expose codebase graph data and UI actions. Protect it with a firewall or trusted network; prefer a specific LAN IP over `0.0.0.0`.
105127
- `CBM_CACHE_DIR` changes both the UI config location and the runtime settings database location.
106128

107129
## 4. Environment Variables

scripts/security-ui.sh

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ set -euo pipefail
55
#
66
# Audits:
77
# A. Frontend asset scan (embedded JS/CSS/HTML or graph-ui/dist/)
8-
# B. HTTP server binding (must be 127.0.0.1 only)
8+
# B. HTTP server binding (localhost by default; non-loopback is explicit)
99
# C. RPC proxy scope (no system()/popen() in HTTP handler path)
1010
# D. CORS check (no wildcard Access-Control-Allow-Origin)
1111
#
@@ -139,6 +139,9 @@ echo "--- B. HTTP server binding check ---"
139139
# audit would go blind, so that is a hard failure, not a skip.
140140
HTTPD="$ROOT/src/ui/httpd.c"
141141
HTTP_SERVER="$ROOT/src/ui/http_server.c"
142+
UI_CONFIG="$ROOT/src/ui/config.h"
143+
UI_CONFIG_C="$ROOT/src/ui/config.c"
144+
MAIN="$ROOT/src/main.c"
142145
for f in "$HTTPD" "$HTTP_SERVER"; do
143146
if [[ ! -f "$f" ]]; then
144147
echo "BLOCKED: expected UI server source missing: $f"
@@ -147,20 +150,37 @@ for f in "$HTTPD" "$HTTP_SERVER"; do
147150
done
148151

149152
if [[ -f "$HTTPD" ]]; then
150-
# Must bind to 127.0.0.1 only
151-
if grep -q '127\.0\.0\.1' "$HTTPD"; then
152-
echo "OK: Server binds to 127.0.0.1"
153+
# The safe default must remain localhost, including when loading an old
154+
# config file that has no host field.
155+
if [[ -f "$UI_CONFIG" ]] && grep -qE '#define CBM_UI_DEFAULT_HOST "127\.0\.0\.1"' "$UI_CONFIG" && \
156+
[[ -f "$UI_CONFIG_C" ]] && grep -q 'CBM_UI_DEFAULT_HOST' "$UI_CONFIG_C"; then
157+
echo "OK: Default UI host is 127.0.0.1"
153158
else
154-
echo "BLOCKED: No 127.0.0.1 binding found in httpd.c"
159+
echo "BLOCKED: Default UI host is not provably 127.0.0.1"
155160
FAIL=1
156161
fi
157162

158-
# Must NOT bind to 0.0.0.0 or INADDR_ANY
159-
if cat "$HTTPD" "$HTTP_SERVER" 2>/dev/null | grep -E '0\.0\.0\.0|INADDR_ANY|in6addr_any' | grep -v '^\s*//' | grep -v '^\s*\*' > /dev/null 2>&1; then
160-
echo "BLOCKED: Server may bind to all interfaces (0.0.0.0/INADDR_ANY found)"
163+
# Non-loopback binding must travel through the explicit numeric IPv4 flag
164+
# and the validated host parameter; this prevents implicit exposure.
165+
if grep -q '"--host="' "$MAIN" && grep -q 'cbm_ui_host_is_valid' "$MAIN" && \
166+
grep -q 'cbm_httpd_listen(const char \*host, int port)' "$HTTPD" && \
167+
grep -q 'inet_pton(AF_INET' "$HTTPD"; then
168+
echo "OK: Non-loopback binding requires explicit validated IPv4 configuration"
169+
else
170+
echo "BLOCKED: Host configuration does not prove explicit validated IPv4 binding"
161171
FAIL=1
172+
fi
173+
174+
# A non-loopback bind must be visibly warned about at the point of use.
175+
if grep -q 'cbm_ui_host_is_loopback' "$HTTP_SERVER" && \
176+
grep -qi 'no authentication' "$HTTP_SERVER" && \
177+
grep -qi 'reachable over the network' "$HTTP_SERVER" && \
178+
grep -qi 'codebase graph data' "$HTTP_SERVER" && \
179+
grep -qi 'firewall' "$HTTP_SERVER"; then
180+
echo "OK: Non-loopback binding emits a prominent security warning"
162181
else
163-
echo "OK: No 0.0.0.0/INADDR_ANY binding"
182+
echo "BLOCKED: Non-loopback security warning is missing or incomplete"
183+
FAIL=1
164184
fi
165185
fi
166186

src/main.c

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* --help Print usage and exit
99
* --ui=true/false Enable/disable HTTP UI server (persisted)
1010
* --port=N Set HTTP UI port (persisted, default 9749)
11+
* --host=IPv4 Set HTTP UI host (persisted, default 127.0.0.1)
1112
*
1213
* Signal handling: SIGTERM/SIGINT trigger graceful shutdown.
1314
* Watcher runs in a background thread, polling for git changes.
@@ -28,6 +29,7 @@ enum {
2829
MAIN_CLI_ARGC = 2,
2930
MAIN_FLAG_OFF = 5, /* strlen("--ui=") */
3031
MAIN_PORT_OFF = 7, /* strlen("--port=") */
32+
MAIN_HOST_OFF = 7, /* strlen("--host=") */
3133
MAIN_MAX_PORT = 65536,
3234
PARENT_WATCHDOG_STACK_SIZE = 64 * CBM_SZ_1K, /* watchdog only polls — tiny stack suffices */
3335
};
@@ -517,6 +519,7 @@ static void print_help(void) {
517519
printf(" --ui=true Enable HTTP graph visualization (persisted)\n");
518520
printf(" --ui=false Disable HTTP graph visualization (persisted)\n");
519521
printf(" --port=N Set UI port (default 9749, persisted)\n");
522+
printf(" --host=<IPv4> Set UI host (default 127.0.0.1, persisted)\n");
520523
printf("\nSupported agents (auto-detected):\n");
521524
printf(" Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode,\n");
522525
printf(" Antigravity, Aider, KiloCode, Kiro\n");
@@ -570,8 +573,10 @@ static int handle_subcommand(int argc, char **argv) {
570573
return CBM_NOT_FOUND;
571574
}
572575

573-
/* Parse --ui= and --port= flags. Returns true if config was modified. */
574-
static bool parse_ui_flags(int argc, char **argv, cbm_ui_config_t *cfg, bool *explicit_enable) {
576+
/* Parse --ui=, --port=, and --host= flags. Returns -1 on invalid input,
577+
* 0 when unchanged, and 1 when config was modified. */
578+
static int parse_ui_flags(int argc, char **argv, cbm_ui_config_t *cfg, bool *explicit_enable,
579+
char *error, size_t error_size) {
575580
bool changed = false;
576581
for (int i = SKIP_ONE; i < argc; i++) {
577582
if (strncmp(argv[i], "--ui=", SLEN("--ui=")) == 0) {
@@ -588,8 +593,22 @@ static bool parse_ui_flags(int argc, char **argv, cbm_ui_config_t *cfg, bool *ex
588593
changed = true;
589594
}
590595
}
596+
if (strcmp(argv[i], "--host") == 0) {
597+
snprintf(error, error_size, "--host requires a numeric IPv4 value (use --host=<IPv4>)");
598+
return -1;
599+
}
600+
if (strncmp(argv[i], "--host=", SLEN("--host=")) == 0) {
601+
const char *host = argv[i] + MAIN_HOST_OFF;
602+
if (!cbm_ui_host_is_valid(host)) {
603+
snprintf(error, error_size,
604+
"invalid --host value '%s'; expected a numeric IPv4 address", host);
605+
return -1;
606+
}
607+
snprintf(cfg->ui_host, sizeof(cfg->ui_host), "%s", host);
608+
changed = true;
609+
}
591610
}
592-
return changed;
611+
return changed ? 1 : 0;
593612
}
594613

595614
/* Install platform-specific signal handlers. */
@@ -722,7 +741,14 @@ int main(int argc, char **argv) {
722741
cbm_ui_config_t ui_cfg;
723742
cbm_ui_config_load(&ui_cfg);
724743
bool explicit_ui_enable = false;
725-
if (parse_ui_flags(argc, argv, &ui_cfg, &explicit_ui_enable)) {
744+
char ui_flag_error[256];
745+
int ui_flags_rc = parse_ui_flags(argc, argv, &ui_cfg, &explicit_ui_enable, ui_flag_error,
746+
sizeof(ui_flag_error));
747+
if (ui_flags_rc < 0) {
748+
(void)fprintf(stderr, "codebase-memory-mcp: %s\n", ui_flag_error);
749+
return 2;
750+
}
751+
if (ui_flags_rc > 0) {
726752
cbm_ui_config_save(&ui_cfg);
727753
}
728754
/* If the user explicitly asked for the UI but this binary has no embedded
@@ -787,7 +813,7 @@ int main(int argc, char **argv) {
787813
bool http_started = false;
788814

789815
if (ui_cfg.ui_enabled && CBM_EMBEDDED_FILE_COUNT > 0) {
790-
g_http_server = cbm_http_server_new(ui_cfg.ui_port);
816+
g_http_server = cbm_http_server_new(ui_cfg.ui_host, ui_cfg.ui_port);
791817
if (g_http_server) {
792818
cbm_http_server_set_watcher(g_http_server, g_watcher);
793819
if (cbm_thread_create(&http_tid, 0, http_thread, g_http_server) == 0) {

src/ui/config.c

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
* config.c — Persistent UI configuration (JSON via yyjson).
33
*
44
* Config file: ~/.cache/codebase-memory-mcp/config.json
5-
* Format: {"ui_enabled": false, "ui_port": 9749}
5+
* Format: {"ui_enabled": false, "ui_port": 9749, "ui_host": "127.0.0.1"}
66
*/
7+
#ifdef _WIN32
8+
#include <winsock2.h>
9+
#include <ws2tcpip.h>
10+
#include <stdatomic.h>
11+
#endif
12+
713
#include "foundation/constants.h"
814
#include "ui/config.h"
915
#include "ui/embedded_assets.h"
@@ -15,9 +21,43 @@
1521
#include <yyjson/yyjson.h>
1622

1723
#include <stdio.h>
24+
#include <stdint.h>
1825
#include <stdlib.h>
1926
#include <string.h>
2027

28+
#ifndef _WIN32
29+
#include <arpa/inet.h>
30+
#endif
31+
32+
#ifdef _WIN32
33+
static void cbm_ui_host_socket_init(void) {
34+
static atomic_int started = 0;
35+
int expected = 0;
36+
if (atomic_compare_exchange_strong(&started, &expected, 1)) {
37+
WSADATA wsa;
38+
(void)WSAStartup(MAKEWORD(2, 2), &wsa);
39+
}
40+
}
41+
#endif
42+
43+
bool cbm_ui_host_is_valid(const char *host) {
44+
struct in_addr addr;
45+
#ifdef _WIN32
46+
cbm_ui_host_socket_init();
47+
#endif
48+
return host && host[0] != '\0' && inet_pton(AF_INET, host, &addr) == 1;
49+
}
50+
51+
bool cbm_ui_host_is_loopback(const char *host) {
52+
struct in_addr addr;
53+
#ifdef _WIN32
54+
cbm_ui_host_socket_init();
55+
#endif
56+
if (!host || inet_pton(AF_INET, host, &addr) != 1)
57+
return false;
58+
return (ntohl(addr.s_addr) & UINT32_C(0xff000000)) == UINT32_C(0x7f000000);
59+
}
60+
2161
/* ── Path ────────────────────────────────────────────────────── */
2262

2363
void cbm_ui_config_path(char *buf, int bufsz) {
@@ -33,6 +73,7 @@ void cbm_ui_config_path(char *buf, int bufsz) {
3373
void cbm_ui_config_load(cbm_ui_config_t *cfg) {
3474
cfg->ui_enabled = CBM_UI_DEFAULT_ENABLED;
3575
cfg->ui_port = CBM_UI_DEFAULT_PORT;
76+
snprintf(cfg->ui_host, sizeof(cfg->ui_host), "%s", CBM_UI_DEFAULT_HOST);
3677

3778
char path[CBM_SZ_1K];
3879
cbm_ui_config_path(path, (int)sizeof(path));
@@ -88,6 +129,14 @@ void cbm_ui_config_load(cbm_ui_config_t *cfg) {
88129
cfg->ui_port = (int)yyjson_get_int(v_port);
89130
}
90131

132+
yyjson_val *v_host = yyjson_obj_get(root, "ui_host");
133+
if (yyjson_is_str(v_host)) {
134+
const char *host = yyjson_get_str(v_host);
135+
if (cbm_ui_host_is_valid(host)) {
136+
snprintf(cfg->ui_host, sizeof(cfg->ui_host), "%s", host);
137+
}
138+
}
139+
91140
yyjson_doc_free(doc);
92141
}
93142

@@ -112,6 +161,8 @@ void cbm_ui_config_save(const cbm_ui_config_t *cfg) {
112161

113162
yyjson_mut_obj_add_bool(doc, root, "ui_enabled", cfg->ui_enabled);
114163
yyjson_mut_obj_add_int(doc, root, "ui_port", cfg->ui_port);
164+
yyjson_mut_obj_add_str(doc, root, "ui_host",
165+
cbm_ui_host_is_valid(cfg->ui_host) ? cfg->ui_host : CBM_UI_DEFAULT_HOST);
115166

116167
size_t json_len = 0;
117168
char *json = yyjson_mut_write(doc, YYJSON_WRITE_PRETTY, &json_len);

src/ui/config.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* config.h — Persistent UI configuration.
33
*
4-
* Stores ui_enabled and ui_port in ~/.cache/codebase-memory-mcp/config.json.
4+
* Stores ui_enabled, ui_port, and ui_host in ~/.cache/codebase-memory-mcp/config.json.
55
* Thread-safe: load/save are independent operations on the filesystem.
66
*/
77
#ifndef CBM_UI_CONFIG_H
@@ -12,12 +12,19 @@
1212
/* Default values */
1313
#define CBM_UI_DEFAULT_PORT 9749
1414
#define CBM_UI_DEFAULT_ENABLED false
15+
#define CBM_UI_DEFAULT_HOST "127.0.0.1"
16+
#define CBM_UI_HOST_MAX 16 /* INET_ADDRSTRLEN: max IPv4 text length + NUL */
1517

1618
typedef struct {
1719
bool ui_enabled;
1820
int ui_port;
21+
char ui_host[CBM_UI_HOST_MAX];
1922
} cbm_ui_config_t;
2023

24+
/* Validate numeric IPv4 text and identify IPv4 loopback addresses. */
25+
bool cbm_ui_host_is_valid(const char *host);
26+
bool cbm_ui_host_is_loopback(const char *host);
27+
2128
/* Load config from disk. Missing/corrupt file → defaults. */
2229
void cbm_ui_config_load(cbm_ui_config_t *cfg);
2330

0 commit comments

Comments
 (0)