PerplexityAgent is designed to be a secure-by-default MCP server. This document maps its controls to the recommendations in the NSA's Model Context Protocol (MCP): Security Design Considerations for AI-Driven Automation (U/OO/6030316-26, May 2026) and describes the threat model and how to report issues.
- A malicious or compromised MCP client sending malformed, oversized, or out-of-range tool parameters.
- Untrusted web content returned by Perplexity that may carry indirect prompt injection aimed at downstream agents.
- Resource-exhaustion / fatigue techniques (prompt storms, recursive requests).
- Secret leakage of the Perplexity API key through tool output or logs.
Out of scope: the security of the Perplexity service itself, and the trust of the operating-system account the server runs under.
| NSA recommendation | How PerplexityAgent implements it |
|---|---|
| Choose supported MCP projects | Built on the official mcp Python SDK (FastMCP). Dependencies are pinned and fully locked in uv.lock. |
| Design for boundaries / least privilege | Default stdio transport runs locally with no network exposure. No shell execution, no filesystem writes (except an optional, explicitly-configured audit-log path). Egress is only to api.perplexity.ai. The API key lives only in the client layer and is never returned by a tool. The MCP server's only egress is api.perplexity.ai; the optional TUI adds an SSRF-guarded page fetcher (see below) that is never reachable from a tool. |
| Validate parameters | Every tool input is validated against a strict pydantic model (schemas.py) with bounded string lengths (≤ 4 KB), numeric ranges (max_results 1–20, num_subquestions 1–8), and a model enum. Unknown fields are rejected (extra="forbid"), preventing parameter smuggling. |
| Constrain & sandbox tool execution | Per-request timeouts, a hard response-size cap, and capped retries with jittered backoff (client.py). Run the process under seccomp/AppArmor/SELinux or in a container for OS-level isolation (see below). |
| Sign & verify messages (transport) | stdio mode is local-trusted. The optional HTTP transport refuses to start without a bearer token, binds to localhost by default, and rejects any request lacking the exact Authorization: Bearer <token> header. Terminate TLS at a reverse proxy in front of it. |
| Filter & monitor outputs / chained execution | Perplexity responses are treated as untrusted input to the next stage. deep_research scans retrieved snippets for indirect-prompt-injection patterns and surfaces them in security_flags. Citations are taken only from API metadata, never from model free-text. |
| Instrument for logging & detection | A structured JSON audit log (security.py) records every tool call and result with redacted parameters, a result hash, validation status, a per-call correlation id (shared by the tool_call/tool_result pair), latency (duration_ms), and token usage when the API reports it — suitable for SIEM ingestion. |
| Track & patch vulnerabilities | Pinned deps + uv.lock; CI runs pip-audit on every push and Dependabot is enabled. GitHub Actions are pinned to commit SHAs (not retargetable tags), and the gitleaks secret scan covers the full git history, not just the worktree. |
| DoS / fatigue resistance | A token-bucket rate limiter (rate_per_minute / rate_burst), bounded sub-question counts, input-size caps, and request timeouts. |
| Access control / token security | PERPLEXITY_API_KEY is loaded server-side from the environment only; the server fails fast if it is absent. No token passthrough. |
The optional perplexity-agent tui (the tui extra) adds a page fetcher
(fetch.py) so commands like /open <url> can pull a page into context — the only
egress path other than api.perplexity.ai. It is reachable only from the
interactive TUI, never from an MCP tool, so the agent-facing threat model and tool
surface are unchanged. Because the fetched URL is attacker-influenceable, the
fetcher applies SSRF + DoS controls mirroring the API client:
- Scheme allowlist — only
http/https;file://,gopher://,data:etc. are rejected. - Private-address rejection — the host is resolved and the fetch is refused if
any resolved IP is private, loopback, link-local (incl.
169.254.169.254cloud metadata), multicast, reserved, or unspecified. This is re-checked on every redirect hop (redirects are followed manually, not by httpx). Default-deny; override only withPERPLEXITY_FETCH_ALLOW_PRIVATE=true. - IP pinning (DNS-rebinding defense) — the connection is made to the exact IP
that passed validation: the request targets the validated address while the
original hostname is sent as the
Hostheader and TLS SNI (so certificate verification still checks the hostname). A resolver that flips public→private between validation and connect cannot change where the socket goes. - Size / time caps — reuses
PERPLEXITY_TIMEOUTandPERPLEXITY_MAX_RESPONSE_BYTES. The byte cap is enforced while the body streams in: an oversizedContent-Lengthis rejected before any read, and a response that exceeds the cap mid-download is aborted, never fully buffered. - Text-only content types — non-text bodies (PDFs, images, archives,
application/octet-stream…) are refused up front; onlytext/*, HTML/XML, and JSON-ish types are fetched, so binaries are never decoded into the prompt. - Untrusted-output handling — extracted page text is run through the same
indirect-prompt-injection scan as
deep_research; matches are surfaced to the user before the text is sent to Sonar. - Audit — fetches share the TUI's
TokenBucketrate limiter and audit logger.
Residual risk: with the connection pinned to the validated IP, the classic DNS-rebinding TOCTOU window is closed. What remains is by design: the fetcher will reach any publicly routable address an attacker-supplied URL points at. Operators who fetch untrusted URLs should still run the TUI behind a filtering egress proxy.
The TUI's local SQLite store (history/tabs/Spaces) holds only the user's own artifacts — no secrets. It is still browsing history, so the file is created owner-only (0600, directory 0700), tabs are deduplicated per space+URL, and each space retains only the newest 50 tabs.
- The API key is stored as a
pydanticSecretStr, kept out ofrepr/logs. - Audit logging recursively redacts keys/tokens/secrets and scrubs inline
Bearer …/pplx-…strings. .envis git-ignored; only.env.example(no secrets) is committed.
- Prefer the default stdio transport. Only enable HTTP behind TLS + a strong, rotated bearer token, and behind a filtering egress proxy / DLP solution.
- Run under an OS sandbox (seccomp / AppArmor / SELinux) or a minimal container with no access to sensitive files or internal networks.
- Set
PERPLEXITY_AUDIT_LOG_PATHand forward the JSON log to your SIEM. - Keep dependencies current; review
pip-auditoutput in CI. - Scale-out note: the token-bucket rate limiter and audit logger are in-process by design — correct for the intended single-user, single-instance deployment. If you ever front the HTTP transport with multiple replicas, rate limiting needs a shared backend (e.g. Redis) and audit logs need centralized collection; neither is built in.
Please report security issues privately to the maintainer rather than opening a public issue: open a confidential issue on the canonical Codeberg repo, or contact the maintainer directly. Include reproduction steps and affected version. Please allow reasonable time for a fix before public disclosure.