From 16ae3fd86492ac62bc928febdfa1b5b54ed8a3de Mon Sep 17 00:00:00 2001 From: Molefi1146 Date: Tue, 30 Sep 2025 20:14:17 +0200 Subject: [PATCH] Add Model Context Protocol (MCP) Python/Atheris fuzzer --- projects/modelcontextprotocol/Dockerfile | 31 +++++ projects/modelcontextprotocol/build.sh | 59 +++++++++ .../modelcontextprotocol/mcp_schema_fuzzer.py | 120 ++++++++++++++++++ projects/modelcontextprotocol/project.yaml | 25 ++++ 4 files changed, 235 insertions(+) create mode 100644 projects/modelcontextprotocol/Dockerfile create mode 100644 projects/modelcontextprotocol/build.sh create mode 100644 projects/modelcontextprotocol/mcp_schema_fuzzer.py create mode 100644 projects/modelcontextprotocol/project.yaml diff --git a/projects/modelcontextprotocol/Dockerfile b/projects/modelcontextprotocol/Dockerfile new file mode 100644 index 000000000000..6563a26cbe79 --- /dev/null +++ b/projects/modelcontextprotocol/Dockerfile @@ -0,0 +1,31 @@ +# Copyright 2025 The OSS-Fuzz project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# OSS-Fuzz integration for Model Context Protocol (MCP) +# Python/Atheris setup per OSS-Fuzz Python guide +FROM gcr.io/oss-fuzz-base/base-builder-python + +# Install any additional Python deps needed by the fuzzer harness +# jsonschema is required by the harness to validate fuzzed JSON against MCP schema +RUN python3 -m pip install --no-cache-dir --upgrade pip \ + && python3 -m pip install --no-cache-dir jsonschema pyinstaller + +# Fetch target repository (schemas and docs) +RUN git clone https://github.com/modelcontextprotocol/modelcontextprotocol.git $SRC/modelcontextprotocol + +# Set workdir to repo (not strictly required, but convenient for builds) +WORKDIR $SRC/modelcontextprotocol + +# Copy build script and fuzzers into $SRC for build step +COPY build.sh mcp_schema_fuzzer.py $SRC/ diff --git a/projects/modelcontextprotocol/build.sh b/projects/modelcontextprotocol/build.sh new file mode 100644 index 000000000000..664619ae572f --- /dev/null +++ b/projects/modelcontextprotocol/build.sh @@ -0,0 +1,59 @@ +#!/bin/bash -eu + +# Copyright 2025 The OSS-Fuzz project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build script for OSS-Fuzz (Python/Atheris) project: modelcontextprotocol +# Reference: https://github.com/google/oss-fuzz/blob/master/docs/getting-started/new-project-guide/python_lang.md + +# Locate the latest MCP JSON schema within the cloned repository. +# Expected structure: $SRC/modelcontextprotocol/schema//schema.json +SCHEMA_DIR=$(ls -d $SRC/modelcontextprotocol/schema/*/ 2>/dev/null | sort | tail -n1 || true) +SCHEMA_JSON="" +if [ -n "${SCHEMA_DIR}" ] && [ -f "${SCHEMA_DIR}/schema.json" ]; then + SCHEMA_JSON="${SCHEMA_DIR}/schema.json" +fi + +# Build fuzzers into $OUT using PyInstaller for a hermetic binary. +# We assume all required Python deps were installed in the Dockerfile. +FUZZER_SRC="$SRC/mcp_schema_fuzzer.py" +FUZZER_NAME_PKG="mcp_schema_fuzzer.pkg" +FUZZER_WRAPPER_NAME="mcp_schema_fuzzer" + +pyinstaller --distpath "$OUT" --onefile --name "$FUZZER_NAME_PKG" "$FUZZER_SRC" + +# Provide schema.json next to the packaged binary if found; the harness prefers +# an adjacent schema.json, but also supports MCP_SCHEMA_PATH env var. +if [ -n "$SCHEMA_JSON" ]; then + cp "$SCHEMA_JSON" "$OUT/schema.json" +fi + +# Create execution wrapper used by OSS-Fuzz +# Note: Because this fuzzer is pure-Python (no native extensions), do NOT set LD_PRELOAD. +cat > "$OUT/$FUZZER_WRAPPER_NAME" << 'EOF' +#!/bin/sh +# LLVMFuzzerTestOneInput for fuzzer detection. +this_dir=$(dirname "$0") +export MCP_SCHEMA_PATH="$this_dir/schema.json" +"$this_dir/mcp_schema_fuzzer.pkg" "$@" +EOF +chmod +x "$OUT/$FUZZER_WRAPPER_NAME" + +# Optionally copy .options or .dict files if present in repository +if ls $SRC/*.options >/dev/null 2>&1; then + cp $SRC/*.options "$OUT/" || true +fi +if ls $SRC/*.dict >/dev/null 2>&1; then + cp $SRC/*.dict "$OUT/" || true +fi diff --git a/projects/modelcontextprotocol/mcp_schema_fuzzer.py b/projects/modelcontextprotocol/mcp_schema_fuzzer.py new file mode 100644 index 000000000000..5c7a0ff0d3c1 --- /dev/null +++ b/projects/modelcontextprotocol/mcp_schema_fuzzer.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Copyright 2025 The OSS-Fuzz project authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import atheris +import sys +import io +import os +import json +from typing import Optional + +# We depend on jsonschema for validation; installed in Dockerfile. +try: + import jsonschema + from jsonschema.validators import validator_for + from jsonschema import FormatChecker +except Exception as e: + # If jsonschema isn't available, raise early during local runs. + raise + + +def _load_schema() -> Optional[dict]: + # Priority: + # 1) MCP_SCHEMA_PATH env var + # 2) schema.json adjacent to the executable (set by build.sh wrapper) + # 3) schema in the repo (useful for local runs) + candidates = [] + + env_path = os.environ.get("MCP_SCHEMA_PATH") + if env_path: + candidates.append(env_path) + + # Adjacent to PyInstaller dist binary + exe_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + candidates.append(os.path.join(exe_dir, "schema.json")) + + # Local repo fallback for manual testing + repo_schema_dir = os.path.join(os.environ.get("SRC", ""), "modelcontextprotocol", "schema") + if os.path.isdir(repo_schema_dir): + # Try to pick the latest subdir if present + try: + subdirs = [os.path.join(repo_schema_dir, d) for d in os.listdir(repo_schema_dir)] + subdirs = [d for d in subdirs if os.path.isdir(d)] + subdirs.sort() + if subdirs: + candidates.append(os.path.join(subdirs[-1], "schema.json")) + except Exception: + pass + + for path in candidates: + try: + if path and os.path.isfile(path): + with open(path, "rb") as f: + return json.load(f) + except Exception: + # Ignore and try next candidate + continue + return None + + +SCHEMA = _load_schema() +VALIDATOR = None +if SCHEMA is not None: + try: + ValidatorClass = validator_for(SCHEMA) + ValidatorClass.check_schema(SCHEMA) + VALIDATOR = ValidatorClass(SCHEMA, format_checker=FormatChecker()) + except Exception: + # If schema is invalid or unsupported, leave VALIDATOR as None. + VALIDATOR = None + + +def TestOneInput(data: bytes) -> None: # LLVMFuzzerTestOneInput name for libFuzzer + # Limit excessively large inputs to keep JSON decoding tractable + if len(data) > 1_000_000: + return + + # Try UTF-8 first; if it fails, attempt latin-1 which maps bytes 1:1. + try: + s = data.decode("utf-8") + except UnicodeDecodeError: + try: + s = data.decode("latin-1") + except Exception: + return + + # Attempt to parse JSON + try: + obj = json.loads(s) + except Exception: + return + + # If we have a compiled validator, validate the object + if VALIDATOR is not None: + try: + VALIDATOR.validate(obj) + except Exception: + # We expect many validation failures; only crashes are interesting + pass + + +def main(): + atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/projects/modelcontextprotocol/project.yaml b/projects/modelcontextprotocol/project.yaml new file mode 100644 index 000000000000..2fb4312fb318 --- /dev/null +++ b/projects/modelcontextprotocol/project.yaml @@ -0,0 +1,25 @@ +# Copyright 2025 The OSS-Fuzz project authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +homepage: https://modelcontextprotocol.io +main_repo: https://github.com/modelcontextprotocol/modelcontextprotocol.git +language: python3 +fuzzing_engines: + - libfuzzer +sanitizers: + - address + - undefined +primary_contact: ramontsengmolefi@gmail.com +# Optional: help_url, issue filing, etc. +help_url: https://github.com/modelcontextprotocol/modelcontextprotocol/issues