Back to skill

Security audit

space-duck-kimi-relay

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed local Kimi login and proxy helper with real credential-handling risks, but its behavior is coherent with its stated purpose and does not show hidden exfiltration or deception.

Install only if you are comfortable storing Kimi tokens locally and routing inference to Moonshot AI infrastructure in China. Treat printed access tokens and proxy secrets as secrets, avoid disabling proxy auth except on a trusted single-user machine, review any KIMI_* endpoint overrides before use, and install the persistent service only when you want the proxy to survive restarts.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (48)

Credential Access

High
Category
Privilege Escalation
Content
- Store path: `~/.kimi-code/credentials/kimi.json` — `CRED_PATH` (kimi_login.py:41-42).
- Written 0600 via `os.open(..., 0o600)` in `_save()` (kimi_login.py:73); dir 0700 (kimi_login.py:65).
- Proxy bearer secret `proxy_secret` written 0600 (kimi_login.py:206).
- Access token read only from local file in `_load()`/`fresh_token()` (kimi_login.py:80-154).

## Outbound hosts (complete egress allowlist)
Every network call is a `urllib.request.urlopen` — there are exactly three call sites:
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```
kimi_login.py login         # interactive device sign-in
kimi_login.py token         # print fresh access token (auto-refresh, file-locked)
kimi_login.py probe         # inference smoke on membership quota
kimi_login.py serve [port]  # local proxy for your runtime (default 8471)
kimi_login.py install-service [port]   # run proxy as a service (systemd user / launchd)
Confidence
83% confidence
Finding
The documented `token` command prints a fresh access token, which creates a direct exfiltration surface because secrets may end up in terminal scrollback, logs, shell history, copied transcripts, or other tooling. Even though this is presented as an operator feature, exposing bearer tokens in plaintext is dangerous because anyone who obtains the token can use the membership until expiry.

Credential Access

High
Category
Privilege Escalation
Content
`login` shows a kimi.com URL + user code; approve it in your browser and
the script stores the tokens. `token` transparently refreshes — Kimi
access tokens live ~15 minutes and **refresh tokens rotate on every
grant**, so always let this script (not ad-hoc curl) do the refreshing;
a stale refresh token is dead after one rotation.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
`login` shows a kimi.com URL + user code; approve it in your browser and
the script stores the tokens. `token` transparently refreshes — Kimi
access tokens live ~15 minutes and **refresh tokens rotate on every
grant**, so always let this script (not ad-hoc curl) do the refreshing;
a stale refresh token is dead after one rotation.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
back to the original box.
- Prefer a local login on every box (`kimi_login.py login`) — that keeps
  the token in local custody where the trust model expects it. Only if
  you cannot log in on the remote box, a short-lived access token from
  `kimi_login.py token` can be used for a brief test: it is valid ~15 min,
  has no refresh capability, and cannot strand any lineage. Treat it like
  any secret — do not paste it into logs, chat, or shared terminals, and
Confidence
80% confidence
Finding
The documentation explicitly recommends that a short-lived access token from `kimi_login.py token` can be used for a brief test. Even with cautions, this normalizes manual token handling and increases the risk of accidental disclosure through chat, notes, remote terminals, clipboard managers, or shell history.

Credential Access

High
Category
Privilege Escalation
Content
Usage:
  kimi_login.py login          # interactive device sign-in
  kimi_login.py token          # print a fresh access token (auto-refresh)
  kimi_login.py probe          # one-line inference smoke on membership quota
  kimi_login.py serve [port]   # local proxy: point your runtime's OpenAI-
                               # compatible base URL at http://127.0.0.1:PORT/v1
Confidence
88% confidence
Finding
The script exposes a mode that prints a fresh access token directly to stdout. On multi-user systems or in environments with shell history, terminal logging, process supervision, or captured command output, this can leak bearer credentials that permit use of the owner's account.

Credential Access

High
Category
Privilege Escalation
Content
def fresh_token():
    """Return a valid access token, refreshing under an exclusive file lock.

    Kimi refresh tokens ROTATE on every grant: two concurrent refreshes
    race and the loser strands the whole lineage. The lock serialises
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def fresh_token():
    """Return a valid access token, refreshing under an exclusive file lock.

    Kimi refresh tokens ROTATE on every grant: two concurrent refreshes
    race and the loser strands the whole lineage. The lock serialises
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
- Metered, default cap 200/day (`FALLBACK_DAILY_CAP`, :210); returns 429 past cap (:322-324).

## Service install (hardened in 0.5.2)
- systemd: secrets written to a **separate 0600 EnvironmentFile** `~/.kimi-code/credentials/relay.env` (kimi_login.py:461), referenced via `EnvironmentFile=` (:465). Secrets are NOT inlined into the (world-readable) unit.
- Deterministic test asserts: secret value absent from unit, `EnvironmentFile=` present, env file 0600. PASS (2026-08-11).
- macOS launchd: plist written 0600 (:444); values embedded in plist (0600) — documented residual.
Confidence
70% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
- Metered, default cap 200/day (`FALLBACK_DAILY_CAP`, :210); returns 429 past cap (:322-324).

## Service install (hardened in 0.5.2)
- systemd: secrets written to a **separate 0600 EnvironmentFile** `~/.kimi-code/credentials/relay.env` (kimi_login.py:461), referenced via `EnvironmentFile=` (:465). Secrets are NOT inlined into the (world-readable) unit.
- Deterministic test asserts: secret value absent from unit, `EnvironmentFile=` present, env file 0600. PASS (2026-08-11).
- macOS launchd: plist written 0600 (:444); values embedded in plist (0600) — documented residual.
Confidence
70% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
- Metered, default cap 200/day (`FALLBACK_DAILY_CAP`, :210); returns 429 past cap (:322-324).

## Service install (hardened in 0.5.2)
- systemd: secrets written to a **separate 0600 EnvironmentFile** `~/.kimi-code/credentials/relay.env` (kimi_login.py:461), referenced via `EnvironmentFile=` (:465). Secrets are NOT inlined into the (world-readable) unit.
- Deterministic test asserts: secret value absent from unit, `EnvironmentFile=` present, env file 0600. PASS (2026-08-11).
- macOS launchd: plist written 0600 (:444); values embedded in plist (0600) — documented residual.
Confidence
70% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Session Persistence

Medium
Category
Rogue Agent
Content
## Service install (hardened in 0.5.2)
- systemd: secrets written to a **separate 0600 EnvironmentFile** `~/.kimi-code/credentials/relay.env` (kimi_login.py:461), referenced via `EnvironmentFile=` (:465). Secrets are NOT inlined into the (world-readable) unit.
- Deterministic test asserts: secret value absent from unit, `EnvironmentFile=` present, env file 0600. PASS (2026-08-11).
- macOS launchd: plist written 0600 (:444); values embedded in plist (0600) — documented residual.

## Overridable endpoints (disclosed)
- `KIMI_AUTH_HOST` (:36), `KIMI_CODING_BASE` (:40), `KIMI_CLIENT_ID` (:38). Changing these re-points token traffic — advanced use, disclosed in frontmatter.
Confidence
79% confidence
Finding
Embedding live secret values in a launchd plist creates persistent on-disk storage of authentication material in a service definition, increasing exposure to local compromise, backups, accidental copying, or operational mishandling. Even with 0600 permissions, this is weaker than keeping secrets outside the service config because the plist becomes a long-lived secret container rather than a simple launcher definition.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill describes capabilities that include reading environment variables, writing credential files, making network requests, and invoking shell/service installation, but it does not declare an explicit tool scope such as permissions or allowed-tools. That mismatch increases the chance an agent framework grants broader access than reviewers expect, enabling credential handling and persistent changes without clear policy boundaries.

Session Persistence

Medium
Category
Rogue Agent
Content
still get valid SSE. Without the env var, failures return the error so
your runtime's own ladder takes over.

For a proxy that survives reboots, `install-service` writes a systemd
user unit (Linux — enable lingering with
`loginctl enable-linger $USER` so it runs while logged out) or a
launchd agent (macOS). Any `OPENROUTER_API_KEY` / `KIMI_*` env vars set
Confidence
80% confidence
Finding
Installing a systemd-user or launchd service creates persistence for a process that holds or can refresh credentials and optionally use a metered fallback API key. Persistence is not inherently malicious here, but it expands the blast radius of compromise by keeping a credential-capable relay running across reboots and logged-out sessions.

External Transmission

Medium
Category
Data Exfiltration
Content
# kimi-cli public client (override if Moonshot rotates it)
CLIENT_ID = os.environ.get("KIMI_CLIENT_ID",
                           "17e5f671-d194-4dfb-9706-5516cb48c098")
CODING_BASE = os.environ.get("KIMI_CODING_BASE", "https://api.kimi.com/coding/v1")
CRED_DIR = os.path.expanduser("~/.kimi-code/credentials")
CRED_PATH = os.path.join(CRED_DIR, "kimi.json")
LOCK_PATH = os.path.join(CRED_DIR, "kimi.lock")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
# kimi-cli public client (override if Moonshot rotates it)
CLIENT_ID = os.environ.get("KIMI_CLIENT_ID",
                           "17e5f671-d194-4dfb-9706-5516cb48c098")
CODING_BASE = os.environ.get("KIMI_CODING_BASE", "https://api.kimi.com/coding/v1")
CRED_DIR = os.path.expanduser("~/.kimi-code/credentials")
CRED_PATH = os.path.join(CRED_DIR, "kimi.json")
LOCK_PATH = os.path.join(CRED_DIR, "kimi.lock")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return {k: os.environ[k] for k in SERVICE_ENV_VARS if os.environ.get(k)}


def _plist_env(env):
    if not env:
        return ""
    kv = "".join(f"\n    <key>{k}</key><string>{v}</string>" for k, v in env.items())
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Static analysis

No suspicious patterns detected.