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 bycontentclass:, 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_WeborPath: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.pyin 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.
Related#
connectors/libs/nn-mcp-core/src/nn_mcp_core/security.py:52-75—sanitize_free_queryimplementationconnectors/mcps/sharepoint/src/sharepoint_mcp/tools/search.py:62,106— call sites in SharePointconnectors/mcps/teams/src/teams_mcp/tools/search.py:43— call site in Teamsconnectors/mcps/outlook-mcp/src/outlook_mcp/utils.py:46— call site in Outlookconnectors/libs/nn-mcp-core/tests/test_security.py:51-84— tests forsanitize_free_query- ADR-0001: Shared Python Library for Cross-Cutting Concerns (where
nn_mcp_corelives)