Note from me: Kindly implement a pre-commit pipeline that runs tools like bandit, semgrep, opengrep, trufflehog on the codebase.
CWE-312 (Cleartext Storage / Transmission of Sensitive Information), CWE-200 (Exposure of Sensitive Information)
Threat Note
/api/settings returns the complete settings model as JSON, including every provider API key the user has configured and the install bearer token itself — the very secret used to authenticate all requests to the backend.
Returning the bearer token from a token-gated endpoint creates a circular trust problem: anyone who gets the token can use it to retrieve the token. More importantly, a compromised renderer, a malicious Electron extension, any same-machine process that obtained the token, or any future auth-bypass vulnerability instantly harvests all stored credentials in a single HTTP call.
The Bug
Affected Files and Lines
| File |
Lines |
Role |
backend/apps/settings/settings.py |
112–114 |
GET handler — dumps the entire model |
backend/apps/settings/models.py |
44–76 |
AppSettings — contains all secret fields |
Evidence
backend/apps/settings/settings.py, lines 112–114:
@settings.router.get("")
async def get_settings():
return load_settings().model_dump() # ← full dump, every field, no redaction
backend/apps/settings/models.py, lines 44–76 — the secret fields returned:
class AppSettings(BaseModel):
anthropic_api_key: Optional[str] = None # ← returned plaintext
openai_api_key: Optional[str] = None # ← returned plaintext
google_api_key: Optional[str] = None # ← returned plaintext
openrouter_api_key: Optional[str] = None # ← returned plaintext
claude_subscription_token: Optional[str] = None # ← returned plaintext
openai_subscription_token: Optional[str] = None # ← returned plaintext
gemini_subscription_token: Optional[str] = None # ← returned plaintext
openswarm_bearer_token: Optional[str] = None # ← THE GATE SECRET, returned plaintext
openswarm_proxy_url: Optional[str] = None
...
The response to a GET /api/settings with the bearer token looks like:
{
"anthropic_api_key": "sk-ant-api03-XXXXXXXX",
"openai_api_key": "sk-XXXXXXXX",
"google_api_key": "AIzaXXXXXX",
"openrouter_api_key": "sk-or-XXXXXXXX",
"openswarm_bearer_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
...
}
Why This Is a Problem
The settings endpoint exists so the UI can pre-fill the settings form. It does not need — and should never return — the raw secret values. A masked representation (sk-ant-…ab12, or simply is_set: true) is sufficient to show the user that a key is configured.
Returning openswarm_bearer_token is self-defeating: the token needed to make the request is handed back by the response. This means the token can be persisted, logged, or observed by any code that can make a single authenticated request.
What a Threat Actor Does
Scenario 1 Compromised Renderer / Malicious Agent Output
An LLM agent that has been manipulated (via prompt injection on a fetched page) or any XSS in the frontend can call GET /api/settings using the token it already has access to (injected via the renderer's fetch/XHR, which carries the stored token). The response delivers all API keys without any additional effort.
Scenario 2 — Same-Machine Process
Any process running as the same OS user can read the token from the filesystem/memory and then immediately call:
curl http://127.0.0.1:8324/api/settings \
-H "Authorization: Bearer <token>"
and receive every cloud API key configured in the app.
Scenario 3 — Chained with SSRF
If an SSRF bypass is ever achieved that allows fetching authenticated internal paths (e.g., via a mis-issued token or a future auth regression), the attacker makes the agent call WebFetch("http://127.0.0.1:8324/api/settings?token=<stolen_token>") and receives all secrets in the agent's context window, from which they can be further exfiltrated.
Existing Safety Measures and Why They Fail
| Measure |
Where |
Why It Fails |
| Bearer token auth |
backend/main.py:89 |
Required to call the endpoint — but the endpoint then hands out that same token plus every other secret |
| Localhost bind |
backend/main.py:789 |
Limits remote access — does nothing to prevent local code from calling the endpoint |
Remediation
Create a SettingsReadView Pydantic model that excludes or masks all secret fields, and return that from the GET endpoint:
SECRET_FIELDS = {
"anthropic_api_key", "openai_api_key", "google_api_key",
"openrouter_api_key", "claude_subscription_token",
"openai_subscription_token", "gemini_subscription_token",
"openswarm_bearer_token", "openswarm_proxy_url",
}
def _mask(value: str | None) -> str | None:
if not value:
return None
return value[:7] + "…" + value[-4:] if len(value) > 11 else "***"
@settings.router.get("")
async def get_settings():
raw = load_settings().model_dump()
for field in SECRET_FIELDS:
if raw.get(field):
raw[field] = _mask(raw[field])
return raw
- Never return
openswarm_bearer_token — remove it from the response entirely.
- The PUT handler can continue accepting the full model for writes.
Note from me: Kindly implement a pre-commit pipeline that runs tools like bandit, semgrep, opengrep, trufflehog on the codebase.
CWE-312 (Cleartext Storage / Transmission of Sensitive Information), CWE-200 (Exposure of Sensitive Information)
Threat Note
/api/settingsreturns the complete settings model as JSON, including every provider API key the user has configured and the install bearer token itself — the very secret used to authenticate all requests to the backend.Returning the bearer token from a token-gated endpoint creates a circular trust problem: anyone who gets the token can use it to retrieve the token. More importantly, a compromised renderer, a malicious Electron extension, any same-machine process that obtained the token, or any future auth-bypass vulnerability instantly harvests all stored credentials in a single HTTP call.
The Bug
Affected Files and Lines
backend/apps/settings/settings.pybackend/apps/settings/models.pyAppSettings— contains all secret fieldsEvidence
backend/apps/settings/settings.py, lines 112–114:backend/apps/settings/models.py, lines 44–76 — the secret fields returned:The response to a
GET /api/settingswith the bearer token looks like:{ "anthropic_api_key": "sk-ant-api03-XXXXXXXX", "openai_api_key": "sk-XXXXXXXX", "google_api_key": "AIzaXXXXXX", "openrouter_api_key": "sk-or-XXXXXXXX", "openswarm_bearer_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ... }Why This Is a Problem
The settings endpoint exists so the UI can pre-fill the settings form. It does not need — and should never return — the raw secret values. A masked representation (
sk-ant-…ab12, or simplyis_set: true) is sufficient to show the user that a key is configured.Returning
openswarm_bearer_tokenis self-defeating: the token needed to make the request is handed back by the response. This means the token can be persisted, logged, or observed by any code that can make a single authenticated request.What a Threat Actor Does
Scenario 1 Compromised Renderer / Malicious Agent Output
An LLM agent that has been manipulated (via prompt injection on a fetched page) or any XSS in the frontend can call
GET /api/settingsusing the token it already has access to (injected via the renderer's fetch/XHR, which carries the stored token). The response delivers all API keys without any additional effort.Scenario 2 — Same-Machine Process
Any process running as the same OS user can read the token from the filesystem/memory and then immediately call:
curl http://127.0.0.1:8324/api/settings \ -H "Authorization: Bearer <token>"and receive every cloud API key configured in the app.
Scenario 3 — Chained with SSRF
If an SSRF bypass is ever achieved that allows fetching authenticated internal paths (e.g., via a mis-issued token or a future auth regression), the attacker makes the agent call
WebFetch("http://127.0.0.1:8324/api/settings?token=<stolen_token>")and receives all secrets in the agent's context window, from which they can be further exfiltrated.Existing Safety Measures and Why They Fail
backend/main.py:89backend/main.py:789Remediation
Create a
SettingsReadViewPydantic model that excludes or masks all secret fields, and return that from the GET endpoint:openswarm_bearer_token— remove it from the response entirely.