Back to skill

Security audit

space-duck-kimi-relay

Security checks for vulnerabilities and agentic risk

Overview

This Kimi relay needs review because configured endpoints can receive credentials without safety checks and concurrent paid retries can exceed its advertised daily cap.

Keep the default HTTPS endpoints and bearer authentication enabled. Enable OpenRouter fallback only if you accept automatic paid requests, and set provider-side spending limits because the local cap is not reliable under concurrency. Install the background service only when wanted, protect printed secrets, and consider the disclosed processing of Kimi conversations in China. The inspected files do not establish malicious behavior.

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
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (59)

Tainted flow: 'req' from os.environ.get (line 51, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

OAuth requests are legitimate, but their destination comes from unrestricted KIMI_AUTH_HOST configuration. A changed destination receives refresh tokens during refresh and controls device-login responses during login; exploitation requires influence over that configuration rather than an ordinary proxy request.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 55)May include surrounding context.

python
"Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": "space-duck-kimi-relay/0.2"})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return r.status, json.loads(r.read() or b"{}")
    except urllib.error.HTTPError as e:
        try:

Tainted flow: 'req' from os.environ.get (line 162, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
92% confidence
Finding

Normal inference transmission to Kimi or the configured OpenRouter fallback is expected, not inherently exfiltration. However, this sink also accepts the unrestricted KIMI_CODING_BASE override and sends a membership bearer token and request content to it, permitting credential and prompt disclosure if configuration is poisoned or mistaken; this overlaps the endpoint-override finding.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 166)May include surrounding context.

python
headers={"Content-Type": "application/json",
                                          "Authorization": "Bearer " + key,
                                          "User-Agent": "space-duck-kimi-relay/0.2"})
    with urllib.request.urlopen(req, timeout=120) as r:
        return r.status, r.read()

Tainted flow: 'req' from os.environ.get (line 348, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
92% confidence
Finding

The streaming path sends the membership bearer token and prompt to the same unrestricted KIMI_CODING_BASE override as the non-streaming path. This is another sink for the endpoint-validation weakness, not evidence of malicious transmission under the default configuration.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 353)May include surrounding context.

python
"Authorization": "Bearer " + key,
                                                  "Accept": "text/event-stream",
                                                  "User-Agent": "space-duck-kimi-relay/0.3"})
            with urllib.request.urlopen(req, timeout=300) as r:
                self.send_response(200)
                self.send_header("Content-Type",
                                 r.headers.get("Content-Type", "text/event-stream"))

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SECURITY-MANIFEST.md (reported line 15)May include surrounding context.

md
- 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:

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The documentation explicitly allows endpoint overrides that repoint token traffic. If those overrides accept arbitrary hosts and plaintext HTTP as the static finding reports, a mistaken or attacker-influenced configuration could disclose access or rotating refresh tokens; the implementation was not supplied, so the missing validation cannot be independently verified. The authentication bypass is explicitly documented and opt-in, so its existence alone is not evidence of a hidden behavior mismatch.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 44)May include surrounding context.

text
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)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 55)May include surrounding context.

md
`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.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 61)May include surrounding context.

Wiring the duck's brain — use the proxy

Kimi access tokens live ~15 minutes, so a static key in your runtime's config will not survive. Run the local proxy instead:

text

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 124)May include surrounding context.

md
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 12)May include surrounding context.

python
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 <redacted-url>

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 125)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 526)May include surrounding context.

python
print("credentials : ❌ not signed in")
    else:
        alive = rec.get("expires_at", 0) - 60 > time.time()
        print(f"credentials : ✅ present ({'access token fresh' if alive else 'will refresh on next use'})")
    try:
        with urllib.request.urlopen(f"<redacted-url>", timeout=3) as r:
            ok = r.status == 200

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SECURITY-MANIFEST.md (reported line 20)May include surrounding context.

md
## Outbound hosts (complete egress allowlist)
Every network call is a `urllib.request.urlopen` — there are exactly three call sites:
- kimi_login.py:55  → `AUTH_HOST` = `https://auth.kimi.com` (OAuth device + token/refresh), :36
- kimi_login.py:166 → `CODING_BASE` = `https://api.kimi.com/coding/v1` (inference), :40
- kimi_login.py:353 → `CODING_BASE` (streaming inference)
- kimi_login.py:333 → `https://openrouter.ai/api/v1` — ONLY when `OPENROUTER_API_KEY` set (:319)
No other socket/urlopen/requests calls exist. **Nothing is sent to Spaceduckling.**

Unsafe Defaults

Medium
Category
Tool Misuse
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.

Content

Scanner excerpt · SECURITY-MANIFEST.md (reported line 40)May include surrounding context.

md
- 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.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · SECURITY-MANIFEST.md (reported line 42)May include surrounding context.

md
## 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.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · SECURITY-MANIFEST.md (reported line 42)May include surrounding context.

md
## 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.

Skill declares no tool scope ('permissions' or 'allowed-tools') but code capabilities were detected: env, file_write, network, shell.

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
80% 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.

Content

Scanner excerpt · SKILL.md (reported line 97)May include surrounding context.

md
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

Unsafe Defaults

Medium
Category
Tool Misuse
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.

Content

Scanner excerpt · SKILL.md (reported line 103)May include surrounding context.

md
launchd agent (macOS). Any `OPENROUTER_API_KEY` / `KIMI_*` env vars set
when you run `install-service` are written to a **separate 0600
`EnvironmentFile`** (`~/.kimi-code/credentials/relay.env`) that the unit
references — secrets are never inlined into the world-readable systemd
unit itself — so the fallback survives reboots without leaking the key.
`status` shows creds, proxy health, and the fallback meter.

Unsafe Defaults

Medium
Category
Tool Misuse
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.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 456)May include surrounding context.

python
os.makedirs(unit_dir, exist_ok=True)
    unit = os.path.join(unit_dir, SERVICE_NAME + ".service")
    # Secrets go in a 0600 EnvironmentFile, NOT inlined into the unit — a unit
    # is world-readable by convention, so an inline OPENROUTER_API_KEY would
    # leak. The proxy reads them from this file via systemd's EnvironmentFile.
    env_line = ""
    if env:

Environment overrides allow credentials and inference data to be sent outside the manifest's declared hosts.

Medium
Category
Excessive Agency
Confidence
98% confidence
Finding

KIMI_AUTH_HOST and KIMI_CODING_BASE accept arbitrary destinations, including plaintext HTTP, despite the metadata's restricted-host assurance. Configuration poisoning or mistakes can therefore disclose rotating refresh tokens, access tokens, and inference content; the default endpoints themselves are appropriate.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 40)May include surrounding context.

python
# 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")

An environment switch disables the bearer protection promised by the manifest.

Medium
Category
Excessive Agency
Confidence
99% confidence
Finding

KIMI_RELAY_NO_AUTH=1 deliberately removes the only application-level authorization check, allowing other local users or processes to consume membership quota and any configured paid fallback. Loopback binding and secure-by-default authentication reduce exposure, but a single-user label does not make all local processes trusted.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 414)May include surrounding context.

python
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())

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · scripts/kimi_login.py (reported line 428)May include surrounding context.

python
if env:
        print("capturing into service env: " + ", ".join(env))
    if sys.platform == "darwin":
        plist_dir = os.path.expanduser("~/Library/LaunchAgents")
        os.makedirs(plist_dir, exist_ok=True)
        plist = os.path.join(plist_dir, "com.spaceduckling.kimi-relay.plist")
        with open(plist, "w") as f: