FlawOpen

flawopen.com/AI Security/rapid7-bulk-export-mcp-graphql-query-injection-cve-2026-97228

● CVE-2026-97228 · CVSS 2.7 · Low
FlawOpen Security Research

CVE-2026-97228: GraphQL Injection in Rapid7 MCP

Rapid7 Bulk Export MCP 0.2.5–0.6.1 interpolated an MCP-supplied export ID into a GraphQL query. Version 0.6.2 parameterized it but used an unsupported ID! type; v0.6.3 changes this to String!. Upgrade to 0.6.3 or later. The issue remains within the operator’s account scope.[1], [2], [6]

💡 Plain English Explainer (ELI5)

A clerk is asked for one labeled folder, but copies the label directly into the database’s instruction sheet. If an untrusted label is treated as part of the instruction instead of plain data, the question can change. The fix keeps the instruction fixed and sends the label separately. Here, every query still runs with the operator’s own Rapid7 permissions; this flaw does not cross account boundaries.[2], [5]

Core Concepts & Subsystem Terms

MCP tool argument
A value supplied to a Model Context Protocol tool call. The value must remain data, even if an AI agent selected or forwarded it.[2]
GraphQL document
The query structure sent to a GraphQL endpoint. User-controlled values should not be concatenated into this syntax.[2], [5]
GraphQL variable
A typed value transmitted separately from the query document, keeping the query structure fixed.[5]
Account scope
The data and actions already authorized by the operator’s Rapid7 API key. The advisory says this issue does not cross tenant or account boundaries.[2]
CWE-943
Improper Neutralization of Special Elements in Data Query Logic; assigned to this CVE by the CNA record.[1], [2], [4]

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-943

Step-by-Step Attack Flow

Step 1

The MCP server has an authenticated Rapid7 API key

The server exposes export-status tools that query the Rapid7 export API using the operator’s configured credentials.[2]

Step 2

An export ID enters the tool call

check_rapid7_export_status and download_rapid7_export pass the identifier to get_export_status; a compromised or careless upstream client can forward untrusted text.[2], [5]

Step 3

String interpolation changes query structure

In versions 0.2.5–0.6.1, the identifier was inserted directly into a GraphQL document. Special characters could alter the query selection instead of remaining a value.[2], [5]

Step 4

The request remains in the operator’s scope

The API evaluates the resulting query with the operator’s own key. Rapid7 says it cannot cross tenant or account boundaries; a malicious operator gains no access they did not already have. The realistic concern is untrusted input forwarded by an upstream MCP client, including indirect prompt-injection paths.[2]

Step 5

Version 0.6.3 fixes the parameter type regression

Version 0.6.2 removed string interpolation but declared $exportId: ID!, which the Rapid7 API rejected as an unknown type. Rapid7’s v0.6.3 release notes identify that regression and change the variable to $exportId: String!. Rapid7’s CVE page still lists 0.6.2 as fixed; use 0.6.3 or later as the first release whose source and release notes show a usable parameterized query.[2], [6], [7]

Source Code: Flaw vs. Secure Implementation

These excerpts follow Rapid7’s source history for get_export_status: the vulnerable version interpolates % export_id into the GraphQL document; v0.6.2 introduced variable binding but declared the unsupported $exportId: ID!; v0.6.3 changes it to $exportId: String!. Rapid7’s CVE page still names 0.6.2 as fixed, while the v0.6.3 release notes identify the regression. Upgrade to 0.6.3 or later. Parameterization keeps user-controlled data out of query syntax. No exploit string is needed to understand or verify the fix.[5], [6]

UNPATCHED FLAW
# Vulnerable source pattern before v0.6.2: export_id is interpolated with %
query = (
    """
    {
      export(id: "%s") {
        id
        status
        dataset
        timestamp
        result {
          prefix
          urls
        }
      }
    }
    """ % export_id
)
response = send_graphql_request(
    endpoint=config["endpoint"],
    api_key=config["api_key"],
    query=query,
)
HARDENED SECURE PATCH
# Fixed in v0.6.3: keep query structure fixed and pass the ID as a String variable
query = """
query GetExportStatus($exportId: String!) {
  export(id: $exportId) {
    id
    status
    dataset
    timestamp
    result {
      prefix
      urls
    }
  }
}
"""
variables = {"exportId": export_id}
response = send_graphql_request(
    endpoint=config["endpoint"],
    api_key=config["api_key"],
    query=query,
    variables=variables,
)

Engineering & System Hardening Checklist

Sources

← AI Security Hub Directory →