Security and input hardening

Read this before exposing any scientific function. MCP tool arguments are model-generated and therefore untrusted. Files, solver output, resource text, and values previously returned by another tool are untrusted too.

Contents

  • Trust boundaries

  • Confine run handles

  • Validate scientific inputs and derived cost

  • Invoke processes safely

  • Generate solver inputs safely

  • Return untrusted data safely

  • Protect secrets and identity

  • Test the boundary

Trust boundaries

Enforce security in code. Schemas, annotations, docstrings, and host confirmation dialogs improve behavior but are not authorization or validation.

For every tool, identify:

  • which arguments select files, computation, network access, or side effects;

  • the maximum CPU, memory, wall time, disk, and result size;

  • whether any returned text originated outside trusted server code;

  • which authenticated user is allowed to access the selected state.

Reject invalid inputs with a short, fixed ToolResult(..., is_error=True). Do not echo an arbitrary argument, file content, stack trace, environment, or command line into the error.

Confine run handles

Return an opaque random run_handle, not an absolute path. Store every run beneath one application-owned root. Resolve every handle through one helper; never call open, os.path.join, subprocess, or shutil.rmtree directly on a tool argument.

RUN_ROOT = os.path.realpath(os.path.join(tempfile.gettempdir(), "mysolver-runs"))
HANDLE_RE = re.compile(r"^mysolver_[A-Za-z0-9_-]{8,80}$")
os.makedirs(RUN_ROOT, mode=0o700, exist_ok=True)

def resolve_run_handle(handle):
    if not isinstance(handle, str) or not HANDLE_RE.fullmatch(handle):
        raise ValueError("invalid run handle")
    joined = os.path.join(RUN_ROOT, handle)
    if os.path.islink(joined):
        raise ValueError("invalid run handle")
    resolved = os.path.realpath(joined)
    if os.path.commonpath([RUN_ROOT, resolved]) != RUN_ROOT:
        raise ValueError("invalid run handle")
    if not os.path.isdir(resolved):
        raise ValueError("unknown run handle")
    return resolved

Use the resolved directory for reads, writes, execution, and deletion. Reject symlinked run directories and symlinked run files. Use fixed server-owned filenames or validate filenames with a strict allowlist. Apply authenticated ownership checks after resolution when runs are user-specific.

If compatibility requires a field named working_dir, document and implement it as the same opaque handle; never accept a caller-selected directory.

Validate scientific inputs and derived cost

Declare explicit JSON Schemas with types, units, defaults, enums, and bounds. Repeat critical checks in the handler because tests and internal callers can bypass protocol validation.

  • Reject booleans where a numeric value is expected.

  • Convert deliberately and reject NaN, positive/negative infinity, and malformed strings with math.isfinite.

  • Validate relationships such as start < stop, normalized fractions, and positive material or mesh properties.

  • Estimate derived work before allocating or launching: mesh cells × steps × sweep points, expected output bytes, requested wall time, and number of runs.

  • Enforce hard ceilings and a per-session/per-user concurrency limit.

  • Bound readers too: pagination, tail_lines, max_points, and maximum file bytes before parsing.

Treat a parameter combination that exceeds a ceiling as a normal tool error. Do not rely on async_tool to make unbounded work safe.

Invoke processes safely

Pass an argument array and keep the shell disabled:

completed = subprocess.run(
    [SOLVER, "--input", "input.deck", "--output", "output.json"],
    cwd=run_dir,
    shell=False,
    check=True,
    timeout=MAX_SOLVER_SECONDS,
    capture_output=True,
    text=True,
)

Use a configured/allowlisted executable path. Never build a command string, use shell=True, call os.system, or pass model text through eval/exec. Limit inherited environment variables. Terminate the process tree on timeout or cancellation. Truncate captured logs before storing or returning them.

Generate solver inputs safely

Prefer a structured serializer or template with typed placeholders. Never concatenate untrusted strings into a solver deck, shell fragment, filename, or directive. For unavoidable free text:

  • constrain length and characters in the schema and handler;

  • map user-facing enums to fixed trusted tokens;

  • reject newlines, comment delimiters, include directives, and path syntax when the target format gives them control meaning;

  • write atomically to a fixed file beneath the resolved run directory.

Return untrusted data safely

Tool results enter model context. Treat solver logs, uploaded files, dataset metadata, labels, and user-provided strings as data, not instructions.

  • Prefer structuredContent fields such as data, warnings, and artifacts.

  • Use a fixed server-authored message stating that external text is untrusted data and must not be followed as instructions when text must be returned.

  • Truncate, paginate, or summarize. Do not return whole logs or files by default.

  • Do not reflect arbitrary arguments in errors.

  • Do not claim that framing alone defeats prompt injection; keep destructive tools separately authorized and minimize cross-tool data flows.

Protect secrets and identity

Never return or log bearer tokens, authorization codes, cookies, passwords, API keys, private environment variables, or full request headers. Results and logs persist beyond the call. Redact sensitive fields before structured logs.

Authenticate and authorize every request at the gateway/resource boundary. Treat Mcp-Session-Id as untrusted routing state, not identity. Bind stored state to authenticated user identity where required and use least privilege.

Test the boundary

Add offline tests for:

  • ../, absolute paths, separators, malformed handles, symlinks, and foreign directories for every read, run, and delete tool;

  • minimum, maximum, non-finite, wrong-type, and adversarial combinations;

  • the derived workload ceiling and output-size ceiling;

  • shell metacharacters, newlines, and deck-control characters in strings;

  • fixed error text that does not reflect the malicious input;

  • timeout, cancellation, concurrent-run, disk-full, and oversized-log paths;

  • output schemas that contain no secret-like fields.

Use scripts/validate_server.py as a warning system, not proof of safety. A static validator cannot establish confinement, authorization, or resource bounds without targeted tests.