[ ⌘K ]
← BACK TO SEARCH

crowterliam/2d6mcp

critical

Generic 2D6 Sci-Fi MCP Server — system-agnostic mechanical engine, dice roller, and rules reference for 2d6-based tabletop RPGs

MCP server (purpose undetermined)

purpose: MCP server (purpose undetermined)threat: network exposed
TypeScript0May 19, 2026May 20, 2026GITHUB
2d6agpldice-rollermcpmodel-context-protocolsci-fisqlitetabletop-rpgtypescript
5/20/2026
high1 finding
src/server.ts
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}
src/server.ts:777

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

high1 finding
src/server.ts
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}
src/server.ts:33src/server.ts:877

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

high1 finding
src/server.ts
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}
src/server.ts:34src/server.ts:793

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

high1 finding
src/server.ts
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}
src/server.ts:857src/server.ts:80-231

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

medium2 findings
src/server.ts
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}
medium1 finding

// Source file not analyzed: src/server.ts

// Finding inferred from import chain: src/server.ts:20-30 → src/server.ts:694-736

src/server.ts:20-30src/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.

medium1 finding

// Source file not analyzed: src/server.ts

// Finding inferred from import chain: src/server.ts:17 → src/server.ts:647

src/server.ts:17src/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.

medium1 finding

// Source file not analyzed: src/server.ts

// Finding inferred from import chain: src/server.ts:15 → src/server.ts:613

src/server.ts:15src/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.

low1 finding
src/server.ts
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}
src/server.ts:33src/server.ts:892

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

shell.execenv.exposurefilesystem.readfilesystem.writeslack.integration
100
LLM-based
low findings+5
high findings+100
medium findings+75