codex-plugin-doctor 1.34.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -402,9 +402,9 @@ jobs:
402
402
  runs-on: ubuntu-latest
403
403
  steps:
404
404
  - uses: actions/checkout@v5
405
- - uses: Esquetta/CodexPluginDoctor@v1.34.0
406
- with:
407
- version: "1.34.0"
405
+ - uses: Esquetta/CodexPluginDoctor@v1.35.0
406
+ with:
407
+ version: "1.35.0"
408
408
  path: .
409
409
  runtime: "true"
410
410
  policy: codex-publish
@@ -30,6 +30,11 @@ function statusFromCheckResult(result) {
30
30
  }
31
31
  return "pass";
32
32
  }
33
+ function isPathWithinRoot(rootPath, candidatePath) {
34
+ const relativePath = path.relative(rootPath, candidatePath);
35
+ return (relativePath === "" ||
36
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
37
+ }
33
38
  export async function readMcpConfigPath(targetPath) {
34
39
  const rootPath = path.resolve(targetPath);
35
40
  const directMcpPath = path.join(rootPath, ".mcp.json");
@@ -40,21 +45,38 @@ export async function readMcpConfigPath(targetPath) {
40
45
  if (!(await fileExists(manifestPath))) {
41
46
  return null;
42
47
  }
48
+ let manifest;
43
49
  try {
44
- const manifest = await readJsonFile(manifestPath);
45
- return typeof manifest.mcpServers === "string"
46
- ? path.resolve(rootPath, manifest.mcpServers)
47
- : null;
50
+ manifest = await readJsonFile(manifestPath);
48
51
  }
49
52
  catch {
50
53
  return null;
51
54
  }
55
+ if (typeof manifest.mcpServers !== "string") {
56
+ return null;
57
+ }
58
+ const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
59
+ if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
60
+ throw new Error("Manifest MCP config path resolves outside the package root.");
61
+ }
62
+ return mcpConfigPath;
52
63
  }
53
64
  async function hasCodexManifest(targetPath) {
54
65
  return fileExists(path.join(path.resolve(targetPath), ".codex-plugin", "plugin.json"));
55
66
  }
56
67
  async function checkGenericMcp(targetPath) {
57
- const mcpConfigPath = await readMcpConfigPath(targetPath);
68
+ let mcpConfigPath;
69
+ try {
70
+ mcpConfigPath = await readMcpConfigPath(targetPath);
71
+ }
72
+ catch (error) {
73
+ return {
74
+ client: "Generic MCP",
75
+ status: "fail",
76
+ summary: error instanceof Error ? error.message : "MCP config path is unsafe.",
77
+ details: ["Keep `.mcp.json` or manifest `mcpServers` references inside the package root."]
78
+ };
79
+ }
58
80
  if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
59
81
  return {
60
82
  client: "Generic MCP",
@@ -193,12 +193,20 @@ async function fileExists(targetPath) {
193
193
  return false;
194
194
  }
195
195
  }
196
+ function isPathWithinRoot(rootPath, candidatePath) {
197
+ const relativePath = path.relative(rootPath, candidatePath);
198
+ return (relativePath === "" ||
199
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
200
+ }
196
201
  async function loadMcpServers(discoveredPackage) {
197
202
  const { manifest, rootPath } = discoveredPackage;
198
203
  if (!manifest.mcpServers) {
199
204
  return null;
200
205
  }
201
206
  const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
207
+ if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
208
+ return null;
209
+ }
202
210
  const exists = await fileExists(mcpConfigPath);
203
211
  if (!exists) {
204
212
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",