Skip to content

ADR-0004: No KQL Field Allowlist in sanitize_free_query#

Status: Accepted Date: 2026-05-01

Context#

Three MCP servers — SharePoint, Teams, and Outlook — accept free-form KQL expressions from agents and pass them to the Microsoft Graph search API. These queries are validated by sanitize_free_query in nn_mcp_core/security.py before being forwarded to Graph.

KQL supports arbitrary field selectors (contentclass:, Path:, SiteId:, contenttype:, size: and many others). An agent — or a prompt-injected payload — could craft a query that uses unexpected field selectors, changing the shape of results returned. This raised the question of whether sanitize_free_query should enforce a field allowlist to constrain which KQL fields are legal.

Decision#

No field allowlist is implemented. sanitize_free_query validates query structure only: it rejects queries that start or end with a logical operator (AND, OR, NOT), contain consecutive operators, or exceed the 500-character length cap. All KQL field selectors pass through unchanged.

Consequences#

Positive#

  • Agents retain the full KQL surface area — scoping by Path:, filtering by contentclass:, combining multiple field selectors — enabling richer reasoning over SharePoint, Teams, and Outlook content.
  • Legitimate fields such as contenttype:, size:, and path-scoped searches are not inadvertently blocked.
  • The implementation remains simple and auditable (nn_mcp_core/security.py:52-75).

Negative#

  • A user or injected payload could craft a KQL query that returns a different result set than intended (e.g. contentclass:STS_Web or Path:https://specific-site).
  • There is no compile-time guarantee that agents will only use "expected" fields.

Risks#

  • Scope expansion within permission boundary: A crafted field selector could widen or redirect results, but only to content the authenticated user can already access. OBO auth (auth.py in each MCP, lines 46-68) ensures every Graph call carries the user's own delegated token; Graph enforces permissions server-side. The blast radius of KQL injection is bounded to the user's own content.

Alternatives Considered#

Field allowlist#

ALLOWED_KQL_FIELDS = {"title", "author", "filename", "filetype",
                      "created", "modified", "description"}

field_pattern = re.compile(r'\b(\w+):', re.IGNORECASE)
for field in field_pattern.findall(query):
    if field.lower() not in ALLOWED_KQL_FIELDS:
        raise SecurityError(f"Field '{field}' is not permitted in search queries")

Rejected because it also blocks legitimate fields such as contenttype:, size:, and Path:-scoped searches that agents genuinely need across SharePoint, Teams, and Outlook. The allowlist would require ongoing maintenance as usage patterns evolve and would degrade the utility of the search tools for agentic workflows without a meaningful security benefit, given that OBO auth is the real access-control boundary.

  • connectors/libs/nn-mcp-core/src/nn_mcp_core/security.py:52-75sanitize_free_query implementation
  • connectors/mcps/sharepoint/src/sharepoint_mcp/tools/search.py:62,106 — call sites in SharePoint
  • connectors/mcps/teams/src/teams_mcp/tools/search.py:43 — call site in Teams
  • connectors/mcps/outlook-mcp/src/outlook_mcp/utils.py:46 — call site in Outlook
  • connectors/libs/nn-mcp-core/tests/test_security.py:51-84 — tests for sanitize_free_query
  • ADR-0001: Shared Python Library for Cross-Cutting Concerns (where nn_mcp_core lives)