flawopen.com/AI Security/rapid7-bulk-export-mcp-graphql-query-injection-cve-2026-97228
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]
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]
Framework mappings
- CWE reference for the weakness described in this guide: CWE-943
Step-by-Step Attack Flow
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]
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]
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]
# 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,
)
# 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
- ✓Inventory Rapid7 Bulk Export MCP deployments and verify the resolved package version, including container images and developer tool environments.[1], [2]
- ✓Upgrade to v0.6.3 or later. Although Rapid7’s CVE page lists 0.6.2 as fixed, its v0.6.3 release notes say the 0.6.2
ID!variable was rejected as an unknown type.[2], [6], [7] - ✓Confirm the status query uses a declared GraphQL variable and passes the export ID through the variables object; do not interpolate tool arguments into query text.[5], [6]
- ✓At the MCP tool boundary, require export_id to be a non-empty string within an explicit integration limit; reject other types and malformed values, then pass accepted IDs only as GraphQL variables. Treat model-generated and retrieved values as untrusted.[2]
- ✓Limit the Rapid7 API key to the required account and actions; do not interpret this issue as cross-tenant access or privilege escalation.[2]
- ✓Add a regression test that submits ordinary identifier data and verifies it is transmitted as a variable while the GraphQL document remains unchanged.[5]
- ✓If call logs are retained, review the affected-version window for unexpected calls to
check_rapid7_export_statusordownload_rapid7_export. For future triage, retain timestamps and tool names with export IDs safely redacted; never log API credentials. This operational recommendation is not evidence that exploitation occurred.[2]
Sources
- Rapid7 vulnerability advisory CVE-2026-97228
- GitHub Advisory GHSA-pwmm-7g3w-8pvp
- Rapid7 Bulk Export MCP v0.6.2 release (introduced parameterization, later type regression)
- CVE record CVE-2026-97228
- Rapid7 fix commit a14e4ff84cbbcc09c565f425f60f83c5d1bb631b
- Rapid7 Bulk Export MCP v0.6.3 release notes (ID! to String! regression fix)
- Rapid7 PR #51 — correct GraphQL variable type from ID! to String!