BACK TO SEARCH
abidc/msgstackcritical

An MCP server for marketing messaging grounding and asset generation via Prefab

MsgStack is an MCP server that provides a structured organizational canon layer for AI grounding. It allows departments to curate approved content (po...

purpose: MsgStack is an MCP server that provides a structurthreat: network exposed
Python · 0 · Jun 8, 2026 · Jun 9, 2026 · GITHUB ↗
RISK SCORE
0/ 100 risk
low findings+5
high findings+50
medium findings+60
capped at100
VULNERABILITY ANALYSIS · 7 findings in 7 blocks2 HIGH · 4 MEDIUM
HIGH1 finding
src/server.py:1
1mcp = FastMCP("MsgStack")
2
3@mcp.tool()
4def search_canon(
5    query: str,
6    section_types: Optional[list[str]] = None,
7    ...
8) -> dict:
9    ...
10    return result
src/server.py:1

// Network-exposed MCP server with no authentication. Any network attacker can call all tools.

EXPLAINThe MCP server exposes all tools without any authentication. The codebase has a full auth system (src/auth.py, src/basic_auth.py) with API keys and scopes, but it is only applied to FastAPI routes, not to the MCP tool endpoints. The MCP server uses FastMCP which does not integrate with the existing auth middleware. The threat model is network_exposed, meaning any network attacker can call all tools without any credentials.
IMPACTAn attacker with network access can call any MCP tool without authentication, including generating artifacts, reading all canon domains, exporting to Penpot, and accessing full organizational content. This completely bypasses the intended access control.
FIXIntegrate authentication into the MCP server. Options: (1) Use FastMCP's built-in auth hooks if available, (2) wrap the MCP server with a middleware that validates API keys before forwarding to FastMCP, (3) require the LLM client to pass an API key in the MCP initialization handshake.
HIGH1 finding
src/server.py:397
397@mcp.tool()
398def generate_artifact(
399    skill_id: str,
400    domain_id: Optional[str] = None,
401    domain_name: Optional[str] = None,
402    custom_context: Optional[dict] = None,
403    ...
404) -> str:
405    ...
406    generator = ArtifactGenerator(store, skills)
407    artifact = generator.generate(skill_id, str(house.id), provided)
src/server.py:397src/pipeline/generator.py

// Network-exposed MCP server. Attacker can call generate_artifact with arbitrary custom_context.

EXPLAINThe generate_artifact tool accepts a custom_context dictionary that is passed directly to the AI generator without any validation or sanitization. This allows an attacker to inject arbitrary data into the generation prompt, potentially leading to prompt injection, data exfiltration, or generation of harmful content. The custom_context is merged with required inputs and passed to the LLM.
IMPACTAn attacker could inject malicious instructions into the AI generation prompt, causing the LLM to ignore its system instructions, generate inappropriate content, or leak sensitive information from the canon domain. Since the threat model is network_exposed, this is exploitable by any network attacker.
FIXValidate custom_context against a whitelist of allowed keys per skill. Reject any unexpected keys. Sanitize string values to prevent prompt injection (e.g., strip control characters, limit length).
MEDIUM1 finding
src/server.py:354
354def _resolve_house(store, house_id: Optional[str], house_name: Optional[str] = None):
355    from uuid import UUID as _UUID
356    house = None
357    if house_id:
358        try:
359            house = store.get_house(_UUID(house_id))
360        except (ValueError, AttributeError):
361            pass
362    if house is None and house_name:
363        house = store.get_house_by_name(house_name)
364    if house is None and house_id:
365        house = store.get_house_by_name(house_id)
366    return house
src/server.py:354

// Network-exposed MCP server. Attacker can call tools with arbitrary domain_id/domain_name.

EXPLAINThe _resolve_house function falls back to looking up by name if UUID parsing fails, and even uses the house_id string as a name if the UUID lookup fails. This means an attacker can pass arbitrary strings as domain_id and the system will attempt to resolve them as names, potentially enumerating valid domain names or causing unexpected behavior. The function silently ignores UUID parsing errors.
IMPACTAn attacker could enumerate valid domain names by observing error messages or timing differences. They could also potentially trigger unexpected behavior by passing specially crafted strings that are neither valid UUIDs nor valid names.
FIXValidate that domain_id is a valid UUID before attempting lookup. Do not fall back to name lookup if UUID is provided but invalid. Return a clear error message if neither UUID nor name resolves.
MEDIUM1 finding
src/server.py:650
650@mcp.tool()
651def export_to_penpot(
652    artifact_id: str,
653    workspace_id: str,
654    domain_id: Optional[str] = None,
655    house_id: Optional[str] = None,
656) -> dict:
657    ...
658    return grounding_tools.export_to_penpot(artifact_id, workspace_id, actual_id)
src/server.py:650src/grounding/tools.py:525

// Network-exposed MCP server. Attacker can call export_to_penpot with arbitrary IDs.

EXPLAINThe export_to_penpot tool accepts artifact_id and workspace_id as arbitrary strings without any validation. These are passed directly to the Penpot export function. An attacker could provide malicious values that could lead to SSRF, data leakage, or manipulation of Penpot projects.
IMPACTAn attacker could potentially export arbitrary artifacts to Penpot projects they shouldn't have access to, or cause the server to make requests to unintended Penpot endpoints (SSRF).
FIXValidate artifact_id and workspace_id as UUIDs. Verify that the artifact belongs to the workspace. Implement proper authorization checks before exporting.
MEDIUM1 finding
src/server.py:677
677@mcp.tool()
678def set_penpot_project(workspace_id: str, project_id: str) -> dict:
679    ...
680    return grounding_tools.set_penpot_project(workspace_id, project_id)
src/server.py:677src/grounding/tools.py:614

// Network-exposed MCP server. Attacker can call set_penpot_project with arbitrary IDs.

EXPLAINThe set_penpot_project tool accepts workspace_id and project_id as arbitrary strings without validation. An attacker could link any Penpot project to any workspace, potentially causing data leakage or unauthorized access to Penpot resources.
IMPACTAn attacker could link a malicious Penpot project to a workspace, causing subsequent exports to go to the wrong project. They could also potentially enumerate valid workspace IDs.
FIXValidate workspace_id and project_id as UUIDs. Verify that the caller has permission to modify the workspace. Consider requiring additional authentication for this sensitive operation.
MEDIUM1 finding
src/server.py:240
240if not department or h.get("department", "General").lower() == department.lower()
241...
242return res
src/server.py:240

// Network-exposed MCP server. Attacker can call list_canon_domains with arbitrary department values.

EXPLAINThe department filter in list_canon_domains uses case-insensitive comparison, but the department field in the data may have inconsistent casing. More importantly, the tool does not validate the department parameter against a whitelist of allowed departments, allowing an attacker to probe for department names by observing which results are returned.
IMPACTAn attacker could enumerate valid department names by passing different values and observing the response. This could leak organizational structure information.
FIXValidate department against a list of known departments. Return a clear error for invalid departments. Consider case-insensitive matching with normalization.
LOW1 finding
src/server.py:908
908@mcp.tool()
909def get_graph_connections(
910    domain_id: Optional[str] = None,
911    persona: Optional[str] = None,
912    channel: Optional[str] = None,
913    house_id: Optional[str] = None,
914) -> dict:
915    ...
916    chunks = engine.get_connections(actual_id, persona=persona, channel=channel)
src/server.py:908

// Network-exposed MCP server. Attacker can call get_graph_connections with arbitrary persona/channel.

EXPLAINThe get_graph_connections tool accepts persona and channel as arbitrary strings without validation against known personas or channels. An attacker could probe for valid persona/channel names by observing the response.
IMPACTAn attacker could enumerate valid persona names and channel names by passing different values and observing the number of results returned.
FIXValidate persona against known personas for the domain. Validate channel against the list of available channels. Return a clear error for invalid values.
6/9/2026
Findings are produced by automated LLM analysis and may include false positives or miss issues. Verify independently before acting.