crowterliam/2d6mcp
criticalGeneric 2D6 Sci-Fi MCP Server — system-agnostic mechanical engine, dice roller, and rules reference for 2d6-based tabletop RPGs
MCP server (purpose undetermined)
777case "parse_character": {
778 const filePath =
779 typeof args?.file_path === "string" ? args.file_path : "";
780 if (!filePath) { ... }
781 const resolvedPath = resolveSafePath(filePath);
782 if (!resolvedPath) { ... }
783 if (!existsSync(resolvedPath)) { ... }
784 try {
785 const content = readFileSync(resolvedPath, "utf-8");
786 const stats = readCharacterFile(content, filePath);
787 return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
788 } catch (err: unknown) { ... }
789}// Network-exposed MCP: exploitable by any attacker who can send tool requests. Local-only MCP: requires compromised LLM to exploit.
The parse_character tool accepts a file_path from user input and passes it to resolveSafePath, which only checks if the resolved path starts with allowed roots. However, the resolveSafePath function uses resolve(filePath) which does not prevent path traversal via symlinks or '..' sequences. An attacker could provide a path like '../../etc/passwd' which resolves to '/etc/passwd' and passes the check if it starts with the project root? Actually, resolve('../../etc/passwd') from the project root would not start with the project root, so it would be blocked. But the check is flawed: it uses resolved.startsWith(root + '/') which can be bypassed if resolved equals root exactly (line 55). More importantly, the function does not canonicalize the path or resolve symlinks, so a symlink inside the allowed root pointing outside could be used. However, the main issue is that the tool reads arbitrary files within the allowed roots, which is beyond the intended purpose of parsing character sheets. The intended purpose is to parse character sheet files, not read arbitrary files. An attacker could read any file within the project or BYOD directory, including configuration files or other sensitive data.
ImpactAn attacker (compromised LLM) could read any file within the project root or BYOD path, potentially exposing sensitive information such as configuration files, credentials, or other user data.
FixRestrict file reading to only files that are actual character sheets (e.g., by checking file extension or content type). Alternatively, remove the file reading capability and require the user to provide the content directly.
98const byodPath = getByodPath();
99const fullPath = join(byodPath, relativePath);
100const resolved = resolve(fullPath);
101
102if (!resolved.startsWith(resolve(byodPath) + "/") && resolved !== resolve(byodPath)) {
103 return {
104 relativePath,
105 fileName: "",
106 ext: "",
107 size: 0,
108 status: "failed",
109 chunks: 0,
110 elapsedMs: 0,
111 message: "Access denied. File must be within the BYOD path.",
112 };
113}// Local-only MCP, requires compromised LLM to exploit
The sync_file tool uses a similar path traversal check that is vulnerable on Windows due to backslash separators. Also, if relativePath contains '..' sequences, join/resolve will normalize them, but the check still uses '/' which fails on Windows. Additionally, symbolic links could bypass the check.
ImpactAn attacker could read and index arbitrary files outside the BYOD directory on Windows, or bypass using symlinks.
FixUse path.relative and check that the result does not start with '..'. Also resolve symlinks.
45function resolveSafePath(filePath: string): string | null {
46 const resolved = resolve(filePath);
47 const allowedRoots: string[] = [resolve(PROJECT_ROOT)];
48
49 const { byodPath } = loadConfig();
50 if (byodPath && existsSync(byodPath)) {
51 allowedRoots.push(resolve(byodPath));
52 }
53
54 for (const root of allowedRoots) {
55 if (resolved === root || resolved.startsWith(root + "/")) {
56 return resolved;
57 }
58 }
59
60 return null;
61}// Local-only MCP, requires compromised LLM to exploit
The resolveSafePath function checks if the resolved path starts with an allowed root plus '/', but on Windows, paths use backslashes. The check `resolved.startsWith(root + '/')` will fail on Windows because the resolved path will contain backslashes. Additionally, symbolic links within the allowed root could bypass the check. The parse_character tool uses this function to restrict file access, but the path traversal protection is incomplete.
ImpactAn attacker could read arbitrary files outside the allowed directories on Windows systems, or bypass the restriction using symlinks on any OS.
FixUse path.normalize and path.sep for cross-platform path checks. Also resolve symlinks before comparison.
857case "sync_file": {
858 const relativePath =
859 typeof args?.relative_path === "string" ? args.relative_path : "";
860 if (!relativePath) { ... }
861 const config = loadConfig();
862 const result = await syncFile(config, relativePath);
863 return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
864}// Network-exposed MCP: exploitable by any attacker. Local-only MCP: requires compromised LLM to exploit.
The sync_file tool accepts a relative_path from user input and passes it to syncFile, which joins it with byodPath and then resolves. The syncFile function (line 99-113) checks if the resolved path starts with the byodPath, but this check can be bypassed using path traversal sequences like '../' because join and resolve normalize the path. For example, if byodPath is '/home/user/byod', a relative_path of '../secret.txt' would resolve to '/home/user/secret.txt', which does not start with '/home/user/byod/' and would be blocked. However, the check uses resolved.startsWith(resolve(byodPath) + '/') which is correct. But the function does not prevent symlink attacks: if a file inside byodPath is a symlink to an outside file, it will be read. Additionally, the function reads the file content via ingestFile, which extracts text from the file. This allows reading arbitrary files that are symlinked into the BYOD directory.
ImpactAn attacker could read arbitrary files on the system by creating a symlink inside the BYOD directory pointing to an external file, then using sync_file to index it. The file content would be extracted and stored in the search index, which can then be retrieved via query_local_byod.
FixUse realpath to resolve symlinks before checking path containment. Also, consider validating that the file is a regular file and not a symlink.
777case "parse_character": {
778 const filePath = typeof args?.file_path === "string" ? args.file_path : "";
779 ...
780 const resolvedPath = resolveSafePath(filePath);
781 ...
782 const content = readFileSync(resolvedPath, "utf-8");
783 const stats = readCharacterFile(content, filePath);
784 ...
785}// Source file not analyzed: src/server.ts
// Finding inferred from import chain: src/server.ts:20-30 → src/server.ts:694-736
// Local-only MCP, requires compromised LLM to exploit
The search_term parameter is passed directly to database query functions without sanitization. If these functions construct SQL queries unsafely, it could lead to SQL injection.
ImpactPotential SQL injection allowing an attacker to read or modify the OGL database.
FixUse parameterized queries or prepared statements in all database query functions.
// Source file not analyzed: src/server.ts
// Finding inferred from import chain: src/server.ts:17 → src/server.ts:647
// Local-only MCP, requires compromised LLM to exploit
The table_name parameter is used directly in database queries without sanitization. If searchOglTables constructs SQL queries unsafely, this could lead to SQL injection. The same applies to other query functions.
ImpactPotential SQL injection allowing an attacker to read or modify the OGL database.
FixUse parameterized queries or prepared statements in all database query functions.
// Source file not analyzed: src/server.ts
// Finding inferred from import chain: src/server.ts:15 → src/server.ts:613
// Local-only MCP, requires compromised LLM to exploit
The roll_custom tool accepts arbitrary dice notation strings and passes them to rollCustom without validation. If rollCustom uses eval or similar to parse the notation, it could lead to arbitrary code execution. Even if it uses a parser, insufficient validation could allow injection of unexpected characters.
ImpactPotential arbitrary code execution if the dice parser is unsafe. At minimum, unexpected behavior or denial of service.
FixValidate the notation against a strict regex (e.g., /^\d*d\d+([+-]\d+)?$/i) before passing to rollCustom.
883case "clear_byod": {
884 const consent = checkByodConsent();
885 if (!consent.allowed) {
886 return {
887 content: [{ type: "text", text: consent.message }],
888 isError: true,
889 };
890 }
891
892 const result = clearByodDatabase();
893 return {
894 content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
895 };
896}// Local-only MCP, requires compromised LLM to exploit
The clear_byod tool deletes the entire BYOD search index database without requiring any additional confirmation beyond the initial BYOD consent. While this is documented behavior, it is a destructive operation that could be triggered accidentally or maliciously by a compromised LLM.
ImpactAn attacker could delete the BYOD index, causing loss of indexed data and requiring re-indexing.
FixAdd a confirmation parameter or require explicit user approval before clearing the database.