mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
mcp: optional CBM_ALLOWED_ROOT boundary for index_repository
index_repository canonicalizes repo_path but imposes no workspace boundary, so a caller-supplied path can resolve anywhere on disk. Add an opt-in CBM_ALLOWED_ROOT: when set, a repo_path that resolves outside it is refused (reusing the same containment check the read sinks use). Unset by default, so the standard index-the-given-path behaviour is unchanged; deployments where repo_path may be influenced by an untrusted caller can set a boundary. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -3842,6 +3842,8 @@ char *cbm_mcp_index_run_supervised_path(const char *root_path) {
|
||||
return index_run_supervised_path(NULL, root_path);
|
||||
}
|
||||
|
||||
bool cbm_path_within_root(const char *root_path, const char *abs_path); /* defined below */
|
||||
|
||||
static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
/* Supervisor gate: run the index in a crash/hang-isolating worker subprocess
|
||||
* unless this process IS the worker or the kill switch (CBM_INDEX_SUPERVISOR=0)
|
||||
@@ -3866,6 +3868,20 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
|
||||
repo_path = canonicalize_repo_path_if_exists(repo_path);
|
||||
|
||||
/* Optional workspace boundary: when CBM_ALLOWED_ROOT is set (agentic /
|
||||
* multi-tenant deployments where repo_path may be influenced by an
|
||||
* untrusted caller), refuse to index a path that resolves outside it.
|
||||
* Unset by default, so the standard "index the path I gave you" behaviour
|
||||
* is unchanged. */
|
||||
const char *allowed_root = getenv("CBM_ALLOWED_ROOT");
|
||||
if (allowed_root && allowed_root[0] && repo_path &&
|
||||
!cbm_path_within_root(allowed_root, repo_path)) {
|
||||
free(mode_str);
|
||||
free(name_override);
|
||||
free(repo_path);
|
||||
return cbm_mcp_text_result("repo_path is outside the allowed root", true);
|
||||
}
|
||||
|
||||
if (mode_str && strcmp(mode_str, "cross-repo-intelligence") == 0) {
|
||||
free(mode_str);
|
||||
free(name_override);
|
||||
|
||||
@@ -4929,6 +4929,35 @@ TEST(detect_changes_rejects_option_like_base_branch) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Opt-in workspace boundary: when CBM_ALLOWED_ROOT is set, index_repository
|
||||
* must refuse a repo_path that resolves outside it. Unset (the default) imposes
|
||||
* no restriction. */
|
||||
TEST(index_repository_honors_allowed_root) {
|
||||
char allowed[512];
|
||||
snprintf(allowed, sizeof(allowed), "%s/cbm_allowed_XXXXXX", cbm_tmpdir());
|
||||
if (!cbm_mkdtemp(allowed)) {
|
||||
FAIL("cbm_mkdtemp failed");
|
||||
}
|
||||
cbm_setenv("CBM_ALLOWED_ROOT", allowed, 1);
|
||||
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
char args[1024];
|
||||
snprintf(args, sizeof(args),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":88,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"index_repository\","
|
||||
"\"arguments\":{\"repo_path\":\"%s/../..\"}}}",
|
||||
allowed); /* resolves to a parent, outside the allowed root */
|
||||
char *resp = cbm_mcp_server_handle(srv, args);
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "outside the allowed root"));
|
||||
free(resp);
|
||||
|
||||
cbm_unsetenv("CBM_ALLOWED_ROOT");
|
||||
cbm_mcp_server_free(srv);
|
||||
cbm_rmdir(allowed);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* SUITE
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
@@ -4936,6 +4965,7 @@ TEST(detect_changes_rejects_option_like_base_branch) {
|
||||
SUITE(mcp) {
|
||||
RUN_TEST(mcp_path_within_root_rejects_escape);
|
||||
RUN_TEST(detect_changes_rejects_option_like_base_branch);
|
||||
RUN_TEST(index_repository_honors_allowed_root);
|
||||
/* JSON-RPC parsing */
|
||||
RUN_TEST(jsonrpc_parse_request);
|
||||
RUN_TEST(jsonrpc_parse_notification);
|
||||
|
||||
Reference in New Issue
Block a user