Token导航 LogoToken导航TokenDH.com
The Postmark MCP NPM Incident logo
开发工具stdio官方级别未说明来源级核验

The Postmark MCP NPM Incident

MCP Server

用于检测和清除恶意npm包postmark-mcp的工具,适用于供应链安全防护。

工具数

0

提示词数

0

GitHub Stars

12

资源数

0
PowerShellPython开发工具

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

作者 / 组织

AdityaBhatt3010

提供方

AdityaBhatt3010

最后核验

2026/5/17 20:21

运行时

Python

快速接入

先看主来源和安装命令,再打开仓库或文档;下面只保留这个条目的关键接入事实。

命令预览

python3 scan_postmark_mcp.py [start_path]

详细介绍

《Postmark-MCP NPM 事件:发生了什么、为何重要以及如何追踪排查》🧐📧

长文速读(TL;DR)状态: 如果 postmark-mcp@1.0.16 如果在你的环境中发现(异常情况),请将其视为已受威胁,并立即采取补救措施。 🚨(警报/紧急情况的符号,无直接对应中文翻译,可理解为“警报”或“紧急情况”)

PostMark-MCP_Cover

______________________________________________________________________

执行摘要

最近的一则披露显示,一个恶意的npm包以 postmark-mcp (用于Postmark的MCP连接器)在发布版本中包含了一个后门 1.0.16 该程序会在发送电子邮件时,悄无声息地将邮件抄送(BCC)至攻击者控制的地址。发现此问题后,该程序包已从注册表中移除,但已安装该版本的系统仍可能在泄露数据。这是一种典型的供应链攻击,影响了连接器工具——其影响重大,因为这些库处理电子邮件(令牌、密码重置、发票)。 😬

______________________________________________________________________

快速事实与影响统计

  • 每周下载量(npm): ~1.5K+ ——这意味着它已被纳入活跃的开发工作流程中,并且可以被许多项目间接包含进来。 📥
  • 预计影响: 保守估计表明 3,000至15,000封电子邮件 本有可能被泄露 日常的;每天的起源于大约 300个组织 (基于观察到的集成情况和下载量进行估算)。这些数字表明,即使是一个下载量适中的软件包,也可能存在大量的数据泄露风险。 📈📨(上升趋势,信件)
  • 妥协的性质: 将邮件静默发送至攻击者控制的邮箱,并将数据泄露至攻击者域名。
  • 为何这很危险: 电子邮件经常携带令牌、凭据、密码重置信息以及个人可识别信息(PII)——因此,窃取邮件意味着可以广泛访问敏感数据。

______________________________________________________________________

妥协指标(Indicators of Compromise,简称IOCs)

  • 包装: postmark-mcp (npm)
  • 恶意版本: 1.0.16 并且后来(恶意发布)
  • 后门邮件(目的地): phan@giftshop[.]club
  • 领域: giftshop[.]club
如果你看到流量到 giftshop.club 或转发至邮件 phan@giftshop.club,将其视为高度可疑。 🔍

______________________________________________________________________

为何这次攻击值得关注(技术层面概述)

MCP连接器是人工智能/电子邮件工具中可信赖的组件。它们:

  1. 处理敏感邮件内容(令牌、发票、内部消息)。
  2. 这些(方案/项目)往往未经深入审查就被纳入项目中。
  3. 可以仅通过最小的代码更改(一行代码即可通过BCC捕获所有内容)来传输大量数据。

结果:中等规模的下载包仍可能导致严重违规。

______________________________________________________________________

防守战术手册——当前应采取的行动

以下是两个实用的扫描工具(Python & PowerShell),用于发现 postmark-mcp 在你的代码库或环境中发生的事件。从工作区根目录运行它们(例如。, ~/Projects) 以识别直接安装、锁定文件引用或明确提及。

如果你检测到版本 1.0.16立即遵循以下修复检查清单。

______________________________________________________________________

Python 扫描器(另存为 scan_postmark_mcp.py)

递归扫描以下内容:

  • package-lock.json / npm-shrinkwrap.json 依存树
  • package.json 条目;条目数
  • node_modules/postmark-mcp/package.json (已安装的副本)
  • 原始文件文本匹配在 .json.js.ts.lock.md.txt
#!/usr/bin/env python3
"""
scan_postmark_mcp.py
Usage:
  python3 scan_postmark_mcp.py [start_path]

Scans recursively for evidence of "postmark-mcp" and flags version 1.0.16 as compromised.
"""
import sys, os, json, re

START = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
TARGET = "postmark-mcp"
COMPROMISED_VERSION = "1.0.16"
findings = []

def check_package_json_file(path):
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            data = json.load(f)
    except Exception:
        return
    for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
        deps = data.get(key) or {}
        if isinstance(deps, dict) and TARGET in deps:
            version = deps.get(TARGET, "unknown")
            findings.append((path, "package.json-deps", version))

def walk_package_lock(path):
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            data = json.load(f)
    except Exception:
        return
    def walk_deps(deps, trail):
        if not isinstance(deps, dict):
            return
        for name, info in deps.items():
            if name == TARGET:
                version = info.get("version") or info.get("resolved") or "unknown"
                findings.append((path + " -> " + " > ".join(trail + [name]), "package-lock", version))
            if isinstance(info, dict):
                nested = info.get("dependencies") or info.get("requires")
                if nested:
                    walk_deps(nested, trail + [name])
    deps = data.get("dependencies") or {}
    walk_deps(deps, [])

def check_node_module_dir(start_dir):
    nm_path = os.path.join(start_dir, "node_modules", TARGET, "package.json")
    if os.path.exists(nm_path):
        try:
            with open(nm_path, "r", encoding="utf-8", errors="ignore") as f:
                data = json.load(f)
            version = data.get("version", "unknown")
        except Exception:
            version = "unknown"
        findings.append((nm_path, "node_modules", version))

def raw_text_check(path):
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            txt = f.read()
    except Exception:
        return
    if TARGET in txt:
        m = re.search(r"postmark-mcp[\"']?\s*[:@]?\s*([0-9]+\.[0-9]+\.[0-9]+)", txt)
        version = m.group(1) if m else "unknown"
        findings.append((path, "raw-text", version))

for root, dirs, files in os.walk(START):
    lower_files = set(files)
    if "package-lock.json" in lower_files:
        walk_package_lock(os.path.join(root, "package-lock.json"))
    if "npm-shrinkwrap.json" in lower_files:
        walk_package_lock(os.path.join(root, "npm-shrinkwrap.json"))
    if "package.json" in lower_files:
        check_package_json_file(os.path.join(root, "package.json"))
    check_node_module_dir(root)
    for fname in files:
        if fname.endswith((".json",".js",".ts",".lock",".txt",".md")):
            fpath = os.path.join(root, fname)
            try:
                if os.path.getsize(fpath) 

param(
  [string]$Path = ".",
  [switch]$AutoUninstall
)

function Ensure-Npm {
  try {
    $npm = & npm --version 2>$null
    if ($LASTEXITCODE -ne 0) { throw "npm not found or not in PATH." }
    return $true
  } catch {
    Write-Warning "npm is not available in PATH. Install Node.js / npm or run this from a machine with npm."
    return $false
  }
}

function Report-And-Uninstall($projectDir, $foundPath, $version) {
  $rel = (Resolve-Path $projectDir).Path
  Write-Host "FOUND: $foundPath  (project root: $rel)  version: $version" -ForegroundColor Yellow

  if (-not (Ensure-Npm)) { return }

  $doIt = $AutoUninstall.IsPresent
  if (-not $doIt) {
    $answer = Read-Host "Uninstall postmark-mcp from $rel? (y/N)"
    $doIt = $answer -match '^(y|yes)$'
  }

  if ($doIt) {
    Write-Host " -> Running: npm uninstall postmark-mcp (in $rel)"
    try {
      Push-Location $rel
      & npm uninstall postmark-mcp 2>&1 | Write-Host
      Pop-Location
      # also attempt to remove node_modules/postmark-mcp folder if it still exists
      $nmPath = Join-Path $rel "node_modules\postmark-mcp"
      if (Test-Path $nmPath) {
        Write-Host " -> Removing leftover folder: $nmPath"
        Remove-Item -Recurse -Force $nmPath -ErrorAction SilentlyContinue
      }
      Write-Host " -> Uninstall attempt finished for $rel" -ForegroundColor Green
    } catch {
      Write-Warning "Uninstall failed for $rel. Error: $_"
      if (Test-Path $rel) { Pop-Location 2>$null }
    }
  } else {
    Write-Host " -> Skipping uninstall for $rel (user chose no)." -ForegroundColor Cyan
  }
}

Write-Host "Scanning for postmark-mcp under: $Path" -ForegroundColor Cyan

# 1) Search package*.json and lockfiles for literal mentions
$packageFiles = Get-ChildItem -Path $Path -Recurse -Include package.json, package*.json, package-lock.json, npm-shrinkwrap.json -File -ErrorAction SilentlyContinue

$hits = @()
foreach ($f in $packageFiles) {
  try {
    $content = Get-Content -Raw -Encoding UTF8 $f.FullName -ErrorAction SilentlyContinue
    if ($content -match "postmark-mcp") {
      # try to extract version if present in JSON
      $version = "unknown"
      try {
        $json = $null
        $json = $content | ConvertFrom-Json -ErrorAction Stop
        if ($json -ne $null) {
          foreach ($k in "dependencies","devDependencies","peerDependencies","optionalDependencies") {
            if ($json.PSObject.Properties.Name -contains $k) {
              $deps = $json.$k
              if ($deps -and $deps["postmark-mcp"]) { $version = $deps["postmark-mcp"] }
            }
          }
        }
      } catch { $version = "unknown" }
      $hits += [PSCustomObject]@{ Path = $f.FullName; Type = "manifest/lockfile"; Version = $version }
    }
  } catch { }
}

# 2) Check node_modules folders for installed copies
$nmDirs = Get-ChildItem -Path $Path -Recurse -Directory -Force -ErrorAction SilentlyContinue |
          Where-Object { $_.FullName -like "*\node_modules\postmark-mcp" -or $_.FullName -like "*/node_modules/postmark-mcp" }

foreach ($d in $nmDirs) {
  $pkg = Join-Path $d.FullName "package.json"
  $ver = "unknown"
  if (Test-Path $pkg) {
    try {
      $pj = Get-Content $pkg -Raw | ConvertFrom-Json
      $ver = $pj.version
    } catch { $ver = "unknown" }
  }
  $projectRoot = ($d.FullName -replace "(.*)(\\|/)node_modules\\postmark-mcp$",'$1')
  $hits += [PSCustomObject]@{ Path = $d.FullName; Type = "node_modules"; Version = $ver; ProjectRoot = $projectRoot }
}

# 3) Print findings
if ($hits.Count -eq 0) {
  Write-Host "No occurrences of postmark-mcp found under: $Path" -ForegroundColor Green
} else {
  Write-Host "Occurrences found:" -ForegroundColor Yellow
  foreach ($h in $hits) {
    if ($h.Type -eq "node_modules") {
      Write-Host " - [node_modules] $($h.Path)  version: $($h.Version)"
    } else {
      Write-Host " - [$($h.Type)] $($h.Path)  version: $($h.Version)"
    }
  }

  # Offer uninstall per project for node_modules hits and manifest hits within a project
  # Collect unique project roots to run npm uninstall inside
  $projectRoots = @()
  foreach ($h in $hits) {
    if ($h.ProjectRoot) { $projectRoots += $h.ProjectRoot }
    else { $projectRoots += (Split-Path $h.Path -Parent) }
  }
  $projectRoots = $projectRoots | Sort-Object -Unique

  foreach ($proj in $projectRoots) {
    $ver = ($hits | Where-Object { ($_.ProjectRoot -eq $proj) -or (Split-Path $_.Path -Parent -eq $proj) } | Select-Object -First 1).Version
    Report-And-Uninstall -projectDir $proj -foundPath $proj -version $ver
  }

  # Also check and uninstall global copy if present
  if (Ensure-Npm) {
    try {
      $globalCheck = & npm ls -g postmark-mcp --depth=0 2>$null
      if ($globalCheck -and $globalCheck -match "postmark-mcp@") {
        Write-Host "`nGlobal postmark-mcp appears installed. Attempt global uninstall?" -ForegroundColor Yellow
        $doGlobal = $AutoUninstall.IsPresent
        if (-not $doGlobal) {
          $ans = Read-Host "Uninstall global postmark-mcp? (y/N)"
          $doGlobal = $ans -match '^(y|yes)$'
        }
        if ($doGlobal) {
          Write-Host " -> Running: npm uninstall -g postmark-mcp"
          & npm uninstall -g postmark-mcp 2>&1 | Write-Host
          Write-Host " -> Global uninstall attempt complete." -ForegroundColor Green
        } else {
          Write-Host " -> Skipping global uninstall." -ForegroundColor Cyan
        }
      } else {
        Write-Host "No global installation of postmark-mcp detected."
      }
    } catch { Write-Warning "Failed to check global npm packages." }
  }
}

Write-Host "`nScan complete." -ForegroundColor Cyan

______________________________________________________________________

如何跑步

扫描当前文件夹:

.\scan_postmark_mcp.ps1

扫描特定文件夹:

.\scan_postmark_mcp.ps1 -Path "C:\Users\Adi\Projects"

自动卸载检测到的副本,无需提示:

.\scan_postmark_mcp.ps1 -Path "C:\Users\Adi\Projects" -AutoUninstall

如果执行被阻塞:

powershell -NoProfile -ExecutionPolicy Bypass -File .\scan_postmark_mcp.ps1

______________________________________________________________________

修复检查清单(如果 1.0.16 (被发现)

  1. 卸载: npm uninstall postmark-mcp (针对每个受影响的项目)。
  2. 全新安装删除 node_modules 加号 package-lock.json 并且运行 npm install 来自一个可信任的锁定文件。
  3. 轮换密钥假设Postmark API密钥、SMTP凭据以及任何可能通过电子邮件发送的令牌均已泄露。 🔑(钥匙)
  4. 审计日志出站邮件日志(查找未知的密送收件人),网络日志(连接至 giftshop.club),以及系统日志。
  5. 全局检查: npm ls -g postmark-mcp --depth=0 并卸载全局副本。
  6. 事件响应记录文件,内部升级处理,并将此视为供应链安全事件。 📝

______________________________________________________________________

最终思考(攻击者+防御者的思维模型)

作为连接器的小型程序包是高价值攻击目标。电子邮件连接器中的一行恶意代码就可能每天在多个组织中泄露数千条信息。关键的防御措施包括自动化依赖项扫描、锁定文件(lockfile)的维护以及快速事件响应。对于任何处理电子邮件的程序都要保持高度警惕。 🛡️🗡️(译文:盾牌&剑)

______________________________________________________________________

目录标签

目录标签

PowerShellPython开发工具本地部署npm安全供应链安全恶意包检测邮件安全

接入字段

传输方式(transport,传输协议)

stdio

鉴权方式(authType,认证方式)

none

运行时(runtime,运行环境)

Python

工具数量(toolCount,工具数)

0

资源数量(resourceCount,资源数)

0

提示词数量(promptCount,提示词数)

0

权限和风险

stdionone部署方式未说明

接入前请确认传输方式、认证方式和部署位置,并根据实际工具能力限制访问范围。

安装前确认

不要直接授予不必要的文件、网络或账号权限;先核对安装命令和配置内容。

来源信息

继续浏览同类 MCP