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, and validation status — suitable for SIEM ingestion. |
| Track & patch vulnerabilities | Pinned deps + uv.lock; CI runs pip-audit on every push. Enable Renovate/Dependabot on the GitHub mirror to automate updates. |
| 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. - Size / time caps — reuses
PERPLEXITY_TIMEOUTandPERPLEXITY_MAX_RESPONSE_BYTES. - 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: a DNS-rebinding TOCTOU window exists between resolution and
connect (we validate the resolved IPs, then connect by hostname for correct TLS).
With fetch_allow_private=False the blast radius is "publicly routable internet
only". 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.
- 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.
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.