This document records the security posture of the Phase 8 plugin and Stellar architecture. It is the result of a review pass over the new code and should be kept in sync as the plugin system grows.
- Fail closed. Where authorization or safety is uncertain, the system denies.
- Capabilities are explicit. No plugin receives a capability implicitly.
- Isolation. A plugin handler failure can never crash a request.
- No fabricated integrations. The Stellar analysis only claims what the indexed files actually demonstrate.
- Configuration over user input. Network endpoints come from environment configuration, never from user or project data.
- Outbound is opt-in. Plugin network access is denied unless an operator explicitly allowlists the target host; private and internal ranges are never reachable by default.
- Read-only by construction. The Stellar/Horizon/RPC surface never signs, simulates, or submits transactions, and never stores or handles keys.
| Threat | Control | Status |
|---|---|---|
| Plugin privilege escalation | Manifests declare capabilities; runtime grants require an explicit CapabilityGrant per workspace (CapabilityStore.grant). Roles are mapped to capabilities in ROLE_CAPABILITY_MAPPING; validate_plugin_capability fails closed. |
Implemented |
| Unauthorized plugin capabilities | Capabilities are validated against the Capability enum before granting; unknown strings are rejected. |
Implemented |
| Workspace isolation | Capability grants are scoped (plugin_id, workspace_id, capability) with a uniqueness constraint. |
Implemented |
| Project isolation | The Stellar AI analysis reuses assert_content_access (owner-only, fails closed). Project files are only ever read for projects the caller may access. |
Implemented |
| GitHub analysis authorization | Stellar-aware PR/issue analysis is detection-driven; the bounded repo slice is fetched through the user's own GitHub token (GitHub's permission model decides access). No manual stellar=true flag exists and no additional access is granted. |
Implemented |
| Prompt-injection resistance (GitHub analysis) | The Stellar PR/issue guidance frames PR/issue text, commit messages, and repository files as untrusted data, not instructions; detection never follows content from untrusted sources. | Implemented |
| Stellar data access | StellarService is read-only; it never signs, sends, or funds. |
Implemented |
| Secret exposure | .env/key files are skipped at project import; the Stellar prompt explicitly flags hard-coded credentials; service config uses env vars only; no secrets are stored or logged. |
Implemented |
| Endpoint manipulation | Endpoint URLs come from current_app.config only. |
Implemented |
| SSRF | validate_endpoint_url: https-only for public networks, loopback-only for custom networks; requests are restricted to the configured Horizon base URL and bounded by timeout + response-size cap. |
Implemented |
| Path traversal | Project file access already rejects traversal at import and in file/tree APIs; Stellar detection only reads path/content of already-indexed files. |
Existing + verified |
| Malicious plugin metadata | Manifest validation rejects bad ids, versions, entry points, unknown capabilities, and empty capability lists. | Implemented |
| Plugin outbound requests (SSRF) | app.services.plugin_network is the only sanctioned outbound path: https-only, denies private/loopback/link-local/reserved IP literals and obviously-private hostnames, denies hostnames that resolve to a private address (best effort), and only reaches hosts on PLUGIN_NETWORK_ALLOWLIST; an empty allowlist denies everything (fail closed). Requests never follow redirects and are timeout/size-bounded. |
Implemented |
| Plugin management authorization | The plugins API is workspace-scoped: members may view, only the owner (manage_plugins) may install/enable/disable/grant. Non-members get 404, non-owners get 403; the server derives authorization from trusted workspace membership, never from client-supplied ownership. |
Implemented |
| Plugin identity binding | Installation binds the plugin to the id declared in the validated manifest; a conflicting entry point for an existing id is rejected (409). Install never loads or executes code and never auto-grants capabilities; grants are restricted to manifest-declared capabilities. | Implemented |
| Event authorization | Before a plugin handler runs, the dispatcher verifies the event type is supported, the plugin exists and is enabled, and for workspace-scoped events the plugin is installed and enabled in the event's workspace, the emitting user is authorized for the workspace, and the plugin holds the capability required by EVENT_CAPABILITY_MAP via an explicit CapabilityGrant for that workspace. Unknown/missing/disabled/unauthorized cases are denied (fail closed); denials are recorded, never delivered. |
Implemented |
| RPC redirect following | StellarService and SorobanRpcClient set allow_redirects=False; any 3xx response is treated as an error (fail closed), so a redirect can never move a validated request to an unvalidated host. |
Implemented |
| RPC host/IP validation | Public-network endpoints must be https, must not be a private/link-local/loopback/reserved IP literal or an obviously-private hostname, and are DNS-verified (best-effort) to resolve to a globally routable address before each request. Custom networks remain loopback-only. |
Implemented |
| RPC out-of-base requests | _check_url restricts requests to the configured Horizon/RPC base URL; anything else is refused. |
Implemented |
| RPC bounds | Timeout (STELLAR_REQUEST_TIMEOUT), response-body cap (STELLAR_MAX_RESPONSE_BYTES), max ledger keys per call (STELLAR_RPC_MAX_KEYS), bounded results, and raw-XDR length caps. |
Implemented |
| RPC malformed responses | Non-JSON-RPC payloads, id mismatches, HTTP errors, and RPC error payloads map to typed SorobanRpc* errors; nothing is silently ignored. |
Implemented |
| Stellar read-only enforcement | SorobanRpcClient implements only read-only methods; sendTransaction/simulateTransaction are absent; the CLI and web APIs only wrap read-only operations. |
Implemented |
| Stellar address validation | Structural G-address checks plus full strkey checksum validation (SEP-23 CRC16-XMODEM) for account/contract inspection; the strkey alphabet correctly includes I/O (only 0, 1, 8, 9 are excluded). |
Implemented |
| Network selection | Users can select only the fixed supported networks (testnet/mainnet/futurenet/local); mainnet is never an implicit default and raw URLs are never accepted. Stored selection only routes read-only requests through STELLAR_NETWORK-derived config. |
Implemented |
| Plugin capability audit | Every grant/revoke/enable/disable and every dispatch denial is appended to the owner-visible ActivityEvent audit trail with plugin/capability/action/outcome (never payloads). |
Implemented |
| Structured error reports | Plugin handler/lifecycle failures are recorded as bounded PluginErrorReport rows (plugin id, operation, exception type, truncated message). No stack traces, payloads, or secrets by default; workspace reports are owner-scoped; recording never affects failure isolation. |
Implemented |
| Config secrecy | Per-workspace plugin config is validated and stored on PluginInstallation.config, is omitted from list/inspect surfaces, and is only read/updated by the workspace owner. |
Implemented |
| Compatibility enforcement | A plugin’s optional PEP 440 compatibility is validated at manifest parse and enforced against the running app version at install; invalid/incompatible plugins are refused. |
Implemented |
| Lifecycle hooks + uninstall | Optional hooks run isolated/non-fatal on registry enable/disable/uninstall; the workspace uninstall endpoint revokes every capability grant for that workspace before removing the installation. | Implemented |
| Plugin events | plugin.enabled/plugin.disabled/plugin.uninstalled are workspace-scoped events mapped to WORKSPACE_READ (dispatch-time enforcement applies). |
Implemented |
| Plugin CLI | flask plugins … acts as the operator on persisted rows; installs are local-path only (URLs refused) and never grant capabilities or load code. |
Implemented |
| Stellar security findings | stellar_security findings are evidence-labelled rows owned by a single project; reads are owner-scoped (404 for non-owners, no existence oracle). |
Implemented |
| XDR decoding | Transaction/envelope + contract-data decoding is bounded and read-only; decoded values are never guessed, malformed/unsupported XDR is explicit, and no signing/submission/keys exist. | Implemented |
Security-relevant coverage includes:
tests/test_stellar_service.py— endpoint validation, SSRF (out-of-base and non-loopback rejection), size caps, error taxonomy.tests/test_stellar_security.py— adversarial endpoint/host validation (private/link-local/reserved IPs, obviously-private hostnames, redirect refusal, request-time host resolution, strict-validation toggle).tests/test_soroban_rpc.py— the RPC client: method behavior, malformed responses, redirect refusal, size caps, private-host resolution rejection, and invalid-parameter fail-closed cases.tests/test_stellar_xdr.py— strkey checksum validation and LedgerKey encoders verified against authoritative Stellar fixtures.
tests/test_stellar_security.py proves the guards fail closed across:
private/loopback/link-local/reserved IP literals (IPv4 + IPv6),
obviously-private hostnames, redirect refusal for both clients, request-time
host resolution via an injected resolver, the strict-validation toggle,
response-size caps, 404/malformed-JSON mapping, and timeout aborts against
a loopback HTTP server that never responds (the clients raise the typed
NetworkError / SorobanRpcUnavailableError instead of hanging).
Host forms are normalized before every check: the scheme and host are
lowercased and a single trailing dot is stripped, so
HTTPS://HORIZON.STELLAR.ORG. is equivalent to https://horizon.stellar.org,
while https://127.0.0.1./ is rejected. Percent-encoded or otherwise unsafe
hosts are rejected outright, so an encoded private literal (e.g.
https://%31%32%37.0.0.1/) can never bypass the literal checks.
DNS bypass limitation: only guard logic is testable offline. A public
hostname that later resolves to a private address is caught at request time by
the injectable host_resolver; real DNS behavior cannot be proven without
network access, and public RPC/Horizon endpoints remain operator-controlled
configuration (see "Known limitations / planned hardening").
tests/test_stellar_inspection.py— account/contract/ledger inspection and honest "unavailable" handling.tests/test_stellar_analysis.py— non-owner and unauthenticated users fail closed; non-Stellar projects are reported honestly.tests/test_stellar_analysis_github.py— detection-driven PR/issue analysis: Stellar context only when detected, non-Stellar and plain-Rust repos stay generic, detection failure never crashes, confidence is respected, context is bounded, and the GitHub routes preserve user-token authorization.tests/test_plugins_manifest.py— malicious/invalid manifests rejected.tests/test_plugin_network.py— the plugin outbound guard: allowlist (exact/*/wildcard), non-https and malformed URLs, loopback/private/ link-local/reserved ranges, obviously-private hostnames, private DNS resolution, theallow_private/strict_dnstoggles, and the boundedguarded_requesthelper (guard runs before any socket is opened).tests/test_capabilities.py— capability grants are explicit, deduplicated, validated, and revoked correctly.tests/test_events.py/tests/test_event_wiring.py— handler failures are isolated and never break the request lifecycle.tests/test_event_authorization.py— dispatch-time capability enforcement: authorized delivery, denial for unknown/disabled/mis-granted plugins, cross-workspace and cross-plugin isolation, missing-context fail-closed, project/workspace consistency (confused-deputy defense), no auto-grant, and the Stellar/AI event capability requirements.tests/test_project_import.py— archive traversal/symlink/size guards.tests/test_stellar_security_findings.py— findings parsing/normalization, persistence, owner read access, and non-Stellar gating.tests/test_stellar_network_switcher.py— validated network selection, mainnet-default protection, persistence, and SSRF-safe routing.tests/test_stellar_mock_network.py— end-to-end reads against the offline mock network.tests/test_stellar_xdr_transaction.py— bounded transaction-envelope XDR decoding (supported ops, unsupported/malformed/truncated cases).tests/test_plugin_integration.py— end-to-end plugin flow and failure-isolation with real dispatch.tests/test_plugin_audit.py— capability/state audit trail, owner-only reads, no sensitive-data leakage.tests/test_plugin_error_reports.py— bounded error recording, dispatch/hook failures, owner-scoped endpoint, operator CLI.tests/test_plugin_config.py— owner-scoped config read/update and validation (unknown keys/types rejected).tests/test_plugin_compat.py— PEP 440 compatibility parsing/enforcement.tests/test_plugin_lifecycle.py— lifecycle hooks and plugin events.tests/test_plugins_cli.py— CLI commands,--json, exit codes, URL refusal, and no implicit grants.
- Custom-network endpoints are restricted to loopback; a production operator using a remote custom node needs to extend the allow-list policy.
- Installing/registering a plugin (API or CLI) never loads or executes plugin code; executing real plugin modules at runtime (auto-loading entry points, subscribing their handlers) is intentionally not wired to any install flow yet — plugins are subscribed in-process by code, and a future runtime must keep capability enforcement and review in front of execution.
- Plugins have no outbound network capability wired up yet. The guard in
app/services/plugin_network.py(and its allowlist policy) is the required path for any future plugin-triggered request; plugin code must callguarded_requestrather thanrequestsdirectly. The DNS-level private-IP check is best-effort (a resolution failure is tolerated unlessPLUGIN_NETWORK_STRICT_DNSis set), so operators should keep the allowlist narrow. - Global events (
github.connected,github.disconnected) carry no workspace context and are delivered to any enabled plugin that subscribes; capability grants are workspace-scoped, so a per-workspace grant check does not apply to them. - Subscribers registered without a
plugin_idare treated as trusted internal handlers and bypass plugin capability checks; plugin installs must always subscribe with their own plugin id. - The DNS-level public-host validation is best-effort (a DNS failure is tolerated because the scheme/literal/base-URL guards still apply). It guards against misconfiguration, not a hostile resolver; public RPC endpoints remain operator-controlled configuration.
- The
/stellarread-only endpoints are login-required and lightly rate limited but not workspace-scoped: they query public network data bound to the configured network (equivalent to a block explorer).