From e9c7bdf567c04628a25a0fce39d91889360c012b Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sun, 19 Jul 2026 10:29:23 -0700 Subject: [PATCH 1/2] fix: close dependency security gaps --- .github/workflows/agent-permission-diff.yml | 4 +- .github/workflows/ci.yml | 4 +- .github/workflows/codeql.yml | 6 +-- .github/workflows/publish.yml | 6 +-- .github/workflows/scorecard.yml | 6 +-- .github/workflows/self-audit.yml | 2 +- CHANGELOG.md | 10 +++++ action.yml | 4 +- pyproject.toml | 2 +- tests/test_supply_chain_policy.py | 42 +++++++++++++++++++++ uv.lock | 14 +++++-- 11 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 tests/test_supply_chain_policy.py diff --git a/.github/workflows/agent-permission-diff.yml b/.github/workflows/agent-permission-diff.yml index 84b3a39..da4d3f5 100644 --- a/.github/workflows/agent-permission-diff.yml +++ b/.github/workflows/agent-permission-diff.yml @@ -19,12 +19,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Scan agent-facing permission changes - uses: saagpatel/agent-permission-diff-bot@v0.5.0 + uses: saagpatel/agent-permission-diff-bot@20bc07d1f8052765d3a65378222a08869a7dd027 # v0.5.0 with: mode: observe fail-on: critical diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07a8467..f24d0c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1286d03..8200f11 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,12 +23,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 05c589e..54ede75 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,9 +18,9 @@ jobs: id-token: write # OIDC trusted publishing steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 with: python-version: "3.11" @@ -28,6 +28,6 @@ jobs: run: uv build - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 with: packages-dir: dist/ diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a4aa36e..dcc0222 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -19,18 +19,18 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Run OpenSSF Scorecard - uses: ossf/scorecard-action@v2.4.3 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: scorecard.sarif results_format: sarif publish_results: true - name: Upload SARIF to code scanning - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: scorecard.sarif diff --git a/.github/workflows/self-audit.yml b/.github/workflows/self-audit.yml index 8586919..8837a46 100644 --- a/.github/workflows/self-audit.yml +++ b/.github/workflows/self-audit.yml @@ -26,7 +26,7 @@ jobs: security-events: write steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Audit MCP servers (local action) uses: ./ diff --git a/CHANGELOG.md b/CHANGELOG.md index df36767..6de62af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security + +- Raised the minimum `mcp` Python SDK version to 1.28.1, closing the vulnerable + WebSocket origin-validation, HTTP session-principal binding, and experimental + task-isolation ranges reported by GHSA-vj7q-gjh5-988w, + GHSA-jpw9-pfvf-9f58, and GHSA-hvrp-rf83-w775. +- Pinned every external GitHub Action used by MCPAudit's CI, CodeQL, + self-audit, Scorecard, composite-action, and PyPI publishing paths to an + immutable commit while retaining the reviewed release version in comments. + ### Added - `proof-before-action` — a local-first CLI that compares a declared action diff --git a/action.yml b/action.yml index 28f2909..41d7de8 100644 --- a/action.yml +++ b/action.yml @@ -84,7 +84,7 @@ runs: using: composite steps: - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.x" @@ -160,7 +160,7 @@ runs: - name: Upload SARIF to code scanning if: ${{ always() && steps.sarif-check.outputs.exists == 'true' }} - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: sarif_file: ${{ steps.scan.outputs.sarif-file }} category: ${{ inputs.sarif-category }} diff --git a/pyproject.toml b/pyproject.toml index 01bb48a..a4fe30b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "rich>=15.0.0", "anyio>=4.0", "json5>=0.10", - "mcp>=1.27.0", + "mcp>=1.28.1", "pydantic>=2.0", "pyyaml>=6.0", ] diff --git a/tests/test_supply_chain_policy.py b/tests/test_supply_chain_policy.py new file mode 100644 index 0000000..7d0e9cc --- /dev/null +++ b/tests/test_supply_chain_policy.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import re +import tomllib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +FULL_COMMIT_SHA = re.compile(r"[0-9a-f]{40}") + + +def test_mcp_runtime_dependency_excludes_known_vulnerable_versions() -> None: + project = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"] + mcp_requirement = next( + requirement for requirement in project["dependencies"] if requirement.startswith("mcp") + ) + + match = re.fullmatch(r"mcp>=(\d+)\.(\d+)\.(\d+)", mcp_requirement) + assert match is not None, "mcp must retain an explicit minimum safe version" + assert tuple(map(int, match.groups())) >= (1, 28, 1) + + +def test_external_github_actions_are_pinned_to_immutable_commits() -> None: + action_files = [ + REPO_ROOT / "action.yml", + *(REPO_ROOT / ".github" / "workflows").glob("*.yml"), + *(REPO_ROOT / ".github" / "workflows").glob("*.yaml"), + ] + + mutable_uses: list[str] = [] + for action_file in action_files: + for line_number, line in enumerate(action_file.read_text(encoding="utf-8").splitlines(), start=1): + match = re.match(r"\s*uses:\s*(\S+)", line) + if match is None: + continue + action_ref = match.group(1) + if action_ref.startswith("./") or action_ref.startswith("docker://"): + continue + _, separator, revision = action_ref.rpartition("@") + if not separator or FULL_COMMIT_SHA.fullmatch(revision) is None: + mutable_uses.append(f"{action_file.relative_to(REPO_ROOT)}:{line_number}: {action_ref}") + + assert not mutable_uses, "external actions must use immutable commit SHAs:\n" + "\n".join(mutable_uses) diff --git a/uv.lock b/uv.lock index 2579608..68337a6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,12 @@ version = 1 revision = 3 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] [[package]] name = "annotated-types" @@ -587,7 +593,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.27.0" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -605,9 +611,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] [[package]] @@ -648,7 +654,7 @@ requires-dist = [ { name = "anyio", specifier = ">=4.0" }, { name = "click", specifier = ">=8.0" }, { name = "json5", specifier = ">=0.10" }, - { name = "mcp", specifier = ">=1.27.0" }, + { name = "mcp", specifier = ">=1.28.1" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=15.0.0" }, From 16009272b4e02197f9dc49d2b5ac30974e326ec8 Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sun, 19 Jul 2026 10:39:17 -0700 Subject: [PATCH 2/2] fix: bind pins to reviewed action releases --- README.md | 2 +- tests/test_supply_chain_policy.py | 45 ++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1d5c761..77254ab 100644 --- a/README.md +++ b/README.md @@ -309,7 +309,7 @@ For an end-to-end generator-to-auditor demo packet, see | Language | Python 3.11+ | | CLI | Click 8 | | Output | Rich | -| MCP protocol | `mcp` SDK 1.27+ | +| MCP protocol | `mcp` SDK 1.28.1+ | | Validation | Pydantic v2 | | Config parsing | PyYAML + json5 | | Watch mode | `watchfiles` (optional extra) | diff --git a/tests/test_supply_chain_policy.py b/tests/test_supply_chain_policy.py index 7d0e9cc..a3c336d 100644 --- a/tests/test_supply_chain_policy.py +++ b/tests/test_supply_chain_policy.py @@ -6,6 +6,22 @@ REPO_ROOT = Path(__file__).resolve().parents[1] FULL_COMMIT_SHA = re.compile(r"[0-9a-f]{40}") +# Resolved from the named releases in each action's owning GitHub repository. +# Changing any row is an explicit supply-chain review event, not a shape-only update. +REVIEWED_ACTION_RELEASES = { + "actions/checkout": ("9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", "v7.0.0"), + "actions/setup-python": ("ece7cb06caefa5fff74198d8649806c4678c61a1", "v6.3.0"), + "astral-sh/setup-uv": ("37802adc94f370d6bfd71619e3f0bf239e1f3b78", "v7.6.0"), + "github/codeql-action/analyze": ("7188fc363630916deb702c7fdcf4e481b751f97a", "v4.37.1"), + "github/codeql-action/init": ("7188fc363630916deb702c7fdcf4e481b751f97a", "v4.37.1"), + "github/codeql-action/upload-sarif": ("7188fc363630916deb702c7fdcf4e481b751f97a", "v4.37.1"), + "ossf/scorecard-action": ("4eaacf0543bb3f2c246792bd56e8cdeffafb205a", "v2.4.3"), + "pypa/gh-action-pypi-publish": ("ba38be9e461d3875417946c167d0b5f3d385a247", "v1.14.1"), + "saagpatel/agent-permission-diff-bot": ( + "20bc07d1f8052765d3a65378222a08869a7dd027", + "v0.5.0", + ), +} def test_mcp_runtime_dependency_excludes_known_vulnerable_versions() -> None: @@ -26,17 +42,34 @@ def test_external_github_actions_are_pinned_to_immutable_commits() -> None: *(REPO_ROOT / ".github" / "workflows").glob("*.yaml"), ] - mutable_uses: list[str] = [] + invalid_uses: list[str] = [] + observed_actions: set[str] = set() for action_file in action_files: for line_number, line in enumerate(action_file.read_text(encoding="utf-8").splitlines(), start=1): - match = re.match(r"\s*uses:\s*(\S+)", line) + match = re.match(r"\s*(?:-\s*)?uses:\s*(\S+)(?:\s+#\s+(\S+))?\s*$", line) if match is None: continue action_ref = match.group(1) if action_ref.startswith("./") or action_ref.startswith("docker://"): continue - _, separator, revision = action_ref.rpartition("@") - if not separator or FULL_COMMIT_SHA.fullmatch(revision) is None: - mutable_uses.append(f"{action_file.relative_to(REPO_ROOT)}:{line_number}: {action_ref}") + action, separator, revision = action_ref.rpartition("@") + release = match.group(2) + expected = REVIEWED_ACTION_RELEASES.get(action) + observed_actions.add(action) + if ( + not separator + or FULL_COMMIT_SHA.fullmatch(revision) is None + or release is None + or expected != (revision, release) + ): + invalid_uses.append( + f"{action_file.relative_to(REPO_ROOT)}:{line_number}: {action_ref}" + f" # {release or ''}" + ) - assert not mutable_uses, "external actions must use immutable commit SHAs:\n" + "\n".join(mutable_uses) + assert not invalid_uses, "external actions must match reviewed release commits:\n" + "\n".join( + invalid_uses + ) + assert observed_actions == set(REVIEWED_ACTION_RELEASES), ( + "reviewed action policy and repository use must stay in sync" + )