返回市场
邮戳-MCP-NPM-事件

邮戳-MCP-NPM-事件

作者:AdityaBhatt30104 星标更新:2025-09-28

项目介绍

Postmark-MCP NPM事件:发生了什么,为什么重要,以及如何追踪它 🧐📧

简要说明: 如果你的环境中出现了 postmark-mcp@1.0.16,请将其视为已受损,并立即进行补救措施。 🚨

PostMark-MCP封面 <br/>


概述

最近披露的一个恶意npm包 postmark-mcp(一个用于Postmark的MCP连接器)在发布版本 1.0.16 中包含了一个后门,该后门会悄悄地将发出的邮件BCC到攻击者控制的地址。该包被发现后从注册表中删除了,但已经安装了该版本的系统仍然可能在泄露数据。这是一个典型的供应链攻击,影响了连接工具——因为这些库处理电子邮件(令牌、密码重置、发票),所以影响很大。 😬


快速事实与影响统计

  • 每周下载量(npm):1.5K+ ——这意味着它处于活跃的开发者工作流程中,并且可能被间接包含在许多项目中。 📥
  • 估计影响: 根据保守计算,每天可能有 3,000–15,000封邮件 被泄露,源自大约 300个组织(基于观察到的集成和下载足迹)。这些数字显示,即使是一个下载量不大的包也可能导致大量数据泄露。 📈📨
  • 攻击性质: 悄悄地将邮件BCC到攻击者控制的邮箱并泄露到攻击者的域。
  • 为什么这很危险: 邮件通常携带令牌、凭证、密码重置信息、个人身份信息(PII)——泄露邮件意味着对敏感信息的广泛访问。

攻击指标(IOC)

  • 包名: postmark-mcp (npm)
  • 恶意版本: 1.0.16 及之后的版本(恶意发布)
  • 后门邮件(目的地): phan@giftshop[.]club
  • 域名: giftshop[.]club

如果你看到流量指向 giftshop.club 或邮件转发至 phan@giftshop.club,请视为高度可疑。 🔍


为何此攻击重要(技术电梯)

MCP连接器是AI/邮件工具使用的信任组件。它们:

  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
用法:
  python3 scan_postmark_mcp.py [开始路径]

递归扫描“postmark-mcp”的证据,并标记版本1.0.16为受损。
"""
import sys, os, json, re

开始 = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
目标 = "postmark-mcp"
受损版本 = "1.0.16"
发现 = []

def 检查_package_json_文件(路径):
    try:
        with open(路径, "r", encoding="utf-8", errors="ignore") as f:
            数据 = json.load(f)
    except Exception:
        return
    for 关键词 in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
        依赖项 = 数据.get(关键词) or {}
        if isinstance(依赖项, dict) and 目标 in 依赖项:
            版本 = 依赖项.get(目标, "未知")
            发现.append((路径, "package.json-deps", 版本))

def 遍历_package_lock(路径):
    try:
        with open(路径, "r", encoding="utf-8", errors="ignore") as f:
            数据 = json.load(f)
    except Exception:
        return
    def 遍历_依赖项(依赖项, 路径):
        if not isinstance(依赖项, dict):
            return
        for 名称, 信息 in 依赖项.items():
            if 名称 == 目标:
                版本 = 信息.get("version") or 信息.get("resolved") or "未知"
                发现.append((路径 + " -> " + " > ".join(路径 + [名称]), "package-lock", 版本))
            if isinstance(信息, dict):
                嵌套 = 信息.get("dependencies") or 信息.get("requires")
                if 嵌套:
                    遍历_依赖项(嵌套, 路径 + [名称])
    依赖项 = 数据.get("dependencies") or {}
    遍历_依赖项(依赖项, [])

def 检查_node_module_目录(起始目录):
    nm_路径 = os.path.join(起始目录, "node_modules", 目标, "package.json")
    if os.path.exists(nm_路径):
        try:
            with open(nm_路径, "r", encoding="utf-8", errors="ignore") as f:
                数据 = json.load(f)
            版本 = 数据.get("version", "未知")
        except Exception:
            版本 = "未知"
        发现.append((nm_路径, "node_modules", 版本))

def 原始文本检查(路径):
    try:
        with open(路径, "r", encoding="utf-8", errors="ignore") as f:
            文本 = f.read()
    except Exception:
        return
    if 目标 in 文本:
        匹配 = re.search(r"postmark-mcp[\"']?\s*[:@]?\s*([0-9]+\.[0-9]+\.[0-9]+)", 文本)
        版本 = 匹配.group(1) if 匹配 else "未知"
        发现.append((路径, "原始文本", 版本))

for 根, 目录, 文件 in os.walk(开始):
    文件集 = set(文件)
    if "package-lock.json" in 文件集:
        遍历_package_lock(os.path.join(根, "package-lock.json"))
    if "npm-shrinkwrap.json" in 文件集:
        遍历_package_lock(os.path.join(根, "npm-shrinkwrap.json"))
    if "package.json" in 文件集:
        检查_package_json_文件(os.path.join(根, "package.json"))
    检查_node_module_目录(根)
    for 文件名 in 文件:
        if 文件名.endswith((".json",".js",".ts",".lock",".txt",".md")):
            文件路径 = os.path.join(根, 文件名)
            try:
                if os.path.getsize(文件路径) <= 1024*1024:
                    原始文本检查(文件路径)
            except Exception:
                pass

if not 发现:
    print("✅ 在以下位置未发现postmark-mcp的证据:", 开始)
else:
    print("⚠️ 在以下位置发现postmark-mcp的证据:", 开始)
    for 路径, 类型, 版本 in 发现:
        是否受损 = (版本 == 损失版本)
        标志 = "🚨 已受损 (1.0.16)" if 是否受损 else ""
        print(f" - [{类型}] {路径}  — 版本: {版本} {标志}")

if 发现:
    print("\n建议下一步操作:")
    print("  1) 对于每个项目,运行 npm uninstall postmark-mcp")
    print("  2) 对于1.0.16:移除 node_modules + package-lock.json 并运行 npm install 新鲜安装")
    print("  3) 旋转相关密钥并审计邮件日志")

如何运行:

python3 scan_postmark_mcp.py /路径/到/开始   # 默认为当前目录

或者

python scan_postmark_mcp.py /路径/到/开始   # 默认为当前目录

<img width="1120" height="188" alt="Python截图" src="https://gips2.baidu.com/it/u=2361160037,3043267302&fm=3081&app=3081&f=PNG?w=1120&h=188" /> <br/>


保存为 scan_postmark_mcp.ps1

<#
.SYNOPSIS
  扫描postmark-mcp的出现情况,并可选地卸载它们。

.DESCRIPTION
  - 递归搜索package*.json和锁文件中的"postmark-mcp"出现情况。
  - 查找node_modules下的已安装副本(*/node_modules/postmark-mcp)。
  - 报告路径和发现的版本(如果有package.json)。
  - 如果使用-AutoUninstall参数,则尝试在项目目录中运行`npm uninstall postmark-mcp`,并在全局安装时运行`npm uninstall -g postmark-mcp`。
  - 如果不使用-AutoUninstall参数,则在卸载每个命中点之前提示确认。

.PARAMETER 路径
  要扫描的根路径。默认为当前目录。

.PARAMETER AutoUninstall
  如果存在,则自动执行卸载操作(无需逐项提示)。

.EXAMPLE
  .\scan_postmark_mcp.ps1 -Path C:\Projects
  .\scan_postmark_mcp.ps1 -AutoUninstall
#>

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

函数 确保Npm {
  try {
    $npm = & npm --version 2>$null
    if ($LASTEXITCODE -ne 0) { throw "npm未找到或不在PATH中。" }
    return $true
  } catch {
    Write-Warning "npm不在PATH中。请安装Node.js/npm或在具有npm的机器上运行此命令。"
    return $false
  }
}

函数 报告并卸载($项目目录, $发现路径, $版本) {
  $相对 = (Resolve-Path $项目目录).Path
  Write-Host "发现:$发现路径  (项目根目录:$相对)  版本:$版本" -ForegroundColor Yellow

  if (-not (确保Npm)) { return }

  $执行 = $AutoUninstall.IsPresent
  if (-not $执行) {
    $答案 = Read-Host "是否从$相对卸载postmark-mcp?(y/N)"
    $执行 = $答案 -match '^(y|yes)$'
  }

  if ($执行) {
    Write-Host " -> 正在运行:npm uninstall postmark-mcp (在$相对)"
    try {
      Push-Location $相对
      & npm uninstall postmark-mcp 2>&1 | Write-Host
      Pop-Location
      # 尝试删除剩余的node_modules/postmark-mcp文件夹
      $nm路径 = Join-Path $相对 "node_modules\postmark-mcp"
      if (Test-Path $nm路径) {
        Write-Host " -> 删除遗留文件夹:$nm路径"
        Remove-Item -Recurse -Force $nm路径 -ErrorAction SilentlyContinue
      }
      Write-Host " -> 卸载尝试完成:$相对" -ForegroundColor Green
    } catch {
      Write-Warning "卸载失败:$相对。错误:$_"
      if (Test-Path $相对) { Pop-Location 2>$null }
    }
  } else {
    Write-Host " -> 跳过卸载:$相对(用户选择不卸载)" -ForegroundColor Cyan
  }
}

Write-Host "正在扫描postmark-mcp:$路径" -ForegroundColor Cyan

# 1) 搜索package*.json和锁文件中的文字提及
$package文件 = Get-ChildItem -Path $路径 -Recurse -Include package.json, package*.json, package-lock.json, npm-shrinkwrap.json -File -ErrorAction SilentlyContinue

$命中 = @()
foreach ($f in $package文件) {
  try {
    $内容 = Get-Content -Raw -Encoding UTF8 $f.FullName -ErrorAction SilentlyContinue
    if ($内容 -match "postmark-mcp") {
      # 尝试提取JSON中的版本
      $版本 = "未知"
      try {
        $json = $null
        $json = $内容 | ConvertFrom-Json -ErrorAction Stop
        if ($json -ne $null) {
          foreach ($k in "dependencies","devDependencies","peerDependencies","optionalDependencies") {
            if ($json.PSObject.Properties.Name -contains $k) {
              $依赖项 = $json.$k
              if ($依赖项 -and $依赖项["postmark-mcp"]) { $版本 = $依赖项["postmark-mcp"] }
            }
          }
        }
      } catch { $版本 = "未知" }
      $命中 += [PSCustomObject]@{ 路径 = $f.FullName; 类型 = "清单/锁文件"; 版本 = $版本 }
    }
  } catch { }
}

# 2) 检查node_modules文件夹中的已安装副本
$nm目录 = Get-ChildItem -Path $路径 -Recurse -Directory -Force -ErrorAction SilentlyContinue |
          Where-Object { $_.FullName -like "*\node_modules\postmark-mcp" -or $_.FullName -like "*/node_modules/postmark-mcp" }

foreach ($d in $nm目录) {
  $pkg = Join-Path $d.FullName "package.json"
  $版本 = "未知"
  if (Test-Path $pkg) {
    try {
      $pj = Get-Content $pkg -Raw | ConvertFrom-Json
      $版本 = $pj.version
    } catch { $版本 = "未知" }
  }
  $项目根目录 = ($d.FullName -replace "(.*)(\\|/)node_modules\\postmark-mcp$",'$1')
  $命中 += [PSCustomObject]@{ 路径 = $d.FullName; 类型 = "node_modules"; 版本 = $版本; 项目根目录 = $项目根目录 }
}

# 3) 打印发现结果
if ($命中.Count -eq 0) {
  Write-Host "未在以下位置发现postmark-mcp的出现情况:$路径" -ForegroundColor Green
} else {
  Write-Host "发现的出现情况:" -ForegroundColor Yellow
  foreach ($h in $命中) {
    if ($h.类型 -eq "node_modules") {
      Write-Host " - [node_modules] $($h.路径)  版本:$($h.版本)"
    } else {
      Write-Host " - [$($h.类型)] $($h.路径)  版本:$($h.版本)"
    }
  }

  # 提供每个项目的卸载选项
  # 收集唯一的项目根目录以在其中运行npm卸载
  $项目根目录 = @()
  foreach ($h in $命中) {
    if ($h.项目根目录) { $项目根目录 += $h.项目根目录 }
    else { $项目根目录 += (Split-Path $h.路径 -Parent) }
  }
  $项目根目录 = $项目根目录 | Sort-Object -Unique

  foreach ($proj in $项目根目录) {
    $版本 = ($命中 | Where-Object { ($_.项目根目录 -eq $proj) -or (Split-Path $_.路径 -Parent -eq $proj) } | Select-Object -First 1).版本
    报告并卸载 -项目目录 $proj -发现路径 $proj -版本 $版本
  }

  # 检查并卸载全局副本(如果存在)
  if (确保Npm) {
    try {
      $全局检查 = & npm ls -g postmark-mcp --depth=0 2>$null
      if ($全局检查 -and $全局检查 -match "postmark-mcp@") {
        Write-Host "`n全局postmark-mcp似乎已安装。尝试全局卸载?" -ForegroundColor Yellow
        $执行全局 = $AutoUninstall.IsPresent
        if (-not $执行全局) {
          $答案 = Read-Host "是否卸载全局postmark-mcp?(y/N)"
          $执行全局 = $答案 -match '^(y|yes)$'
        }
        if ($执行全局) {
          Write-Host " -> 正在运行:npm uninstall -g postmark-mcp"
          & npm uninstall -g postmark-mcp 2>&1 | Write-Host
          Write-Host " -> 全局卸载尝试完成。" -ForegroundColor Green
        } else {
          Write-Host " -> 跳过全局卸载。" -ForegroundColor Cyan
        }
      } else {
        Write-Host "未检测到全局安装的postmark-mcp。"
      }
    } catch { Write-Warning "无法检查全局npm包。" }
  }
}

Write-Host "`n扫描完成。" -ForegroundColor Cyan

如何运行

扫描当前文件夹: