@getnote/mcp
Advanced tools
+53
-3
@@ -52,2 +52,51 @@ #!/usr/bin/env node | ||
| const SERVER_VERSION = pkg.version; | ||
| // ─── Security ──────────────────────────────────────────────────────────────── | ||
| /** | ||
| * 校验并解析 upload_image 的 image_path,防止路径穿越(安全漏洞 #10)。 | ||
| * | ||
| * 为什么要校验:upload_image 会把 image_path 指向的本地文件读出来并上传到公网 | ||
| * CDN。如果不校验,攻击者只要能发起 MCP 调用,就能传入任意路径 | ||
| * (如 "/etc/passwd"、"../../.ssh/id_rsa"),把宿主机上的任意敏感文件读出来 | ||
| * 并外泄到 CDN —— 等同于任意文件读取漏洞。 | ||
| * | ||
| * 校验策略: | ||
| * - 拒绝 null byte:底层 syscall 会在 \0 处截断字符串,可绕过后续校验。 | ||
| * - 拒绝绝对路径(以 / 或 \ 开头,或平台判定为绝对路径)。 | ||
| * - 拒绝包含 ".." 的路径段:避免跳出工作目录 / 允许目录。 | ||
| * - 若设置了 GETNOTE_MCP_ALLOWED_ROOT:用 realpathSync + resolve 解开符号链接 | ||
| * 后,确保最终真实路径严格位于该目录内(防止软链逃逸)。 | ||
| * - 若未设置:默认只允许相对路径(上面已拒绝绝对路径与 ".."),相对当前 | ||
| * 工作目录读取。 | ||
| * | ||
| * @returns 经校验、可安全读取的最终路径。 | ||
| */ | ||
| function resolveSafeImagePath(imagePath) { | ||
| if (typeof imagePath !== "string" || imagePath.length === 0) { | ||
| throw new Error("image_path must be a non-empty string"); | ||
| } | ||
| // null byte 会截断底层路径字符串,从而绕过后续的字符串校验 | ||
| if (imagePath.includes("\0")) { | ||
| throw new Error("image_path must not contain null bytes"); | ||
| } | ||
| // 绝对路径直接拒绝(覆盖 POSIX 的 "/"、Windows 的 "\" 与盘符) | ||
| if ((0, node_path_1.isAbsolute)(imagePath) || /^[/\\]/.test(imagePath)) { | ||
| throw new Error("image_path must be a relative path; absolute paths are not allowed"); | ||
| } | ||
| // 任意 ".." 路径段都可能用来跳出允许目录 | ||
| if (imagePath.split(/[/\\]/).includes("..")) { | ||
| throw new Error('image_path must not contain ".." path segments'); | ||
| } | ||
| const allowedRoot = process.env.GETNOTE_MCP_ALLOWED_ROOT; | ||
| if (allowedRoot) { | ||
| // realpathSync 解开符号链接,防止用软链把路径指向允许目录之外的文件 | ||
| const rootReal = (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(allowedRoot)); | ||
| const target = (0, node_fs_1.realpathSync)((0, node_path_1.resolve)(rootReal, imagePath)); | ||
| if (target !== rootReal && !target.startsWith(rootReal + node_path_1.sep)) { | ||
| throw new Error("image_path resolves outside of GETNOTE_MCP_ALLOWED_ROOT"); | ||
| } | ||
| return target; | ||
| } | ||
| // 未配置允许目录:仅允许相对路径(已校验),相对 cwd 解析 | ||
| return (0, node_path_1.resolve)(process.cwd(), imagePath); | ||
| } | ||
| // ─── Config ────────────────────────────────────────────────────────────────── | ||
@@ -384,3 +433,3 @@ function getApiKey() { | ||
| type: "string", | ||
| description: "本地图片文件路径", | ||
| description: "本地图片文件路径。出于安全考虑仅接受相对路径:不允许绝对路径(以 / 或 \\ 开头)、不允许包含 \"..\" 的路径段、不允许 null byte。默认相对当前工作目录解析;若设置了环境变量 GETNOTE_MCP_ALLOWED_ROOT,则路径必须落在该目录内(含符号链接解析后)。", | ||
| }, | ||
@@ -700,4 +749,5 @@ image_base64: { | ||
| if (input.image_path) { | ||
| // 从文件路径读取 | ||
| imageData = fs.readFileSync(input.image_path); | ||
| // 从文件路径读取 —— 先做路径安全校验,防止任意文件读取(漏洞 #10) | ||
| const safePath = resolveSafeImagePath(input.image_path); | ||
| imageData = fs.readFileSync(safePath); | ||
| } | ||
@@ -704,0 +754,0 @@ else if (input.image_base64) { |
+5
-4
| { | ||
| "name": "@getnote/mcp", | ||
| "version": "1.5.0", | ||
| "version": "1.5.1", | ||
| "description": "MCP server for 得到大脑(Get笔记) — manage notes and knowledge bases from AI assistants", | ||
@@ -27,3 +27,3 @@ "main": "dist/index.js", | ||
| "axios": "^1.16.1", | ||
| "form-data": "^4.0.0", | ||
| "form-data": "^4.0.6", | ||
| "zod": "^3.22.0" | ||
@@ -40,8 +40,9 @@ }, | ||
| "express-rate-limit": "^8.2.2", | ||
| "hono": "^4.12.18", | ||
| "hono": "^4.12.21", | ||
| "path-to-regexp": "^8.4.1", | ||
| "fast-uri": "^3.1.2", | ||
| "ip-address": "^10.2.0", | ||
| "axios": "^1.16.1" | ||
| "axios": "^1.16.1", | ||
| "qs": "^6.15.2" | ||
| } | ||
| } |
+64
-5
@@ -19,4 +19,4 @@ #!/usr/bin/env node | ||
| import { GetNoteClient, GetNoteAPIError, SaveNoteReq, UpdateNoteReq } from "./client.js"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { readFileSync, realpathSync } from "node:fs"; | ||
| import { join, resolve, isAbsolute, sep } from "node:path"; | ||
@@ -28,2 +28,59 @@ const pkg = JSON.parse( | ||
| // ─── Security ──────────────────────────────────────────────────────────────── | ||
| /** | ||
| * 校验并解析 upload_image 的 image_path,防止路径穿越(安全漏洞 #10)。 | ||
| * | ||
| * 为什么要校验:upload_image 会把 image_path 指向的本地文件读出来并上传到公网 | ||
| * CDN。如果不校验,攻击者只要能发起 MCP 调用,就能传入任意路径 | ||
| * (如 "/etc/passwd"、"../../.ssh/id_rsa"),把宿主机上的任意敏感文件读出来 | ||
| * 并外泄到 CDN —— 等同于任意文件读取漏洞。 | ||
| * | ||
| * 校验策略: | ||
| * - 拒绝 null byte:底层 syscall 会在 \0 处截断字符串,可绕过后续校验。 | ||
| * - 拒绝绝对路径(以 / 或 \ 开头,或平台判定为绝对路径)。 | ||
| * - 拒绝包含 ".." 的路径段:避免跳出工作目录 / 允许目录。 | ||
| * - 若设置了 GETNOTE_MCP_ALLOWED_ROOT:用 realpathSync + resolve 解开符号链接 | ||
| * 后,确保最终真实路径严格位于该目录内(防止软链逃逸)。 | ||
| * - 若未设置:默认只允许相对路径(上面已拒绝绝对路径与 ".."),相对当前 | ||
| * 工作目录读取。 | ||
| * | ||
| * @returns 经校验、可安全读取的最终路径。 | ||
| */ | ||
| function resolveSafeImagePath(imagePath: string): string { | ||
| if (typeof imagePath !== "string" || imagePath.length === 0) { | ||
| throw new Error("image_path must be a non-empty string"); | ||
| } | ||
| // null byte 会截断底层路径字符串,从而绕过后续的字符串校验 | ||
| if (imagePath.includes("\0")) { | ||
| throw new Error("image_path must not contain null bytes"); | ||
| } | ||
| // 绝对路径直接拒绝(覆盖 POSIX 的 "/"、Windows 的 "\" 与盘符) | ||
| if (isAbsolute(imagePath) || /^[/\\]/.test(imagePath)) { | ||
| throw new Error( | ||
| "image_path must be a relative path; absolute paths are not allowed" | ||
| ); | ||
| } | ||
| // 任意 ".." 路径段都可能用来跳出允许目录 | ||
| if (imagePath.split(/[/\\]/).includes("..")) { | ||
| throw new Error('image_path must not contain ".." path segments'); | ||
| } | ||
| const allowedRoot = process.env.GETNOTE_MCP_ALLOWED_ROOT; | ||
| if (allowedRoot) { | ||
| // realpathSync 解开符号链接,防止用软链把路径指向允许目录之外的文件 | ||
| const rootReal = realpathSync(resolve(allowedRoot)); | ||
| const target = realpathSync(resolve(rootReal, imagePath)); | ||
| if (target !== rootReal && !target.startsWith(rootReal + sep)) { | ||
| throw new Error( | ||
| "image_path resolves outside of GETNOTE_MCP_ALLOWED_ROOT" | ||
| ); | ||
| } | ||
| return target; | ||
| } | ||
| // 未配置允许目录:仅允许相对路径(已校验),相对 cwd 解析 | ||
| return resolve(process.cwd(), imagePath); | ||
| } | ||
| // ─── Config ────────────────────────────────────────────────────────────────── | ||
@@ -378,3 +435,4 @@ | ||
| type: "string", | ||
| description: "本地图片文件路径", | ||
| description: | ||
| "本地图片文件路径。出于安全考虑仅接受相对路径:不允许绝对路径(以 / 或 \\ 开头)、不允许包含 \"..\" 的路径段、不允许 null byte。默认相对当前工作目录解析;若设置了环境变量 GETNOTE_MCP_ALLOWED_ROOT,则路径必须落在该目录内(含符号链接解析后)。", | ||
| }, | ||
@@ -724,4 +782,5 @@ image_base64: { | ||
| if (input.image_path) { | ||
| // 从文件路径读取 | ||
| imageData = fs.readFileSync(input.image_path as string); | ||
| // 从文件路径读取 —— 先做路径安全校验,防止任意文件读取(漏洞 #10) | ||
| const safePath = resolveSafeImagePath(input.image_path as string); | ||
| imageData = fs.readFileSync(safePath); | ||
| } else if (input.image_base64) { | ||
@@ -728,0 +787,0 @@ // 从 Base64 解码 |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
134063
5.23%3204
3.45%9
12.5%Updated