T03 · Remote Payload Retrieval and Execution
- Location
scripts/telegram_listener.py:140- Finding
Platform-Signed Action Messages Can Execute Arbitrary Bash Without Per-Command Consent
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This skill has a coherent Space Duck networking purpose, but it includes broad local control paths that deserve manual review before installation.
Install only if you are comfortable giving Space Duck persistent local listeners, access to a Beak Key, and potential workspace Markdown synchronization. Prefer strict consent, avoid Run all/24-hour approvals, disable or avoid broad workspace snapshots unless needed, do not use pipe-to-shell bootstrap commands, and review any Telegram/Mission Control action before allowing it to run shell commands.
scripts/telegram_listener.py:140Platform-Signed Action Messages Can Execute Arbitrary Bash Without Per-Command Consent
scripts/telegram_listener.py:431Twenty-Four-Hour Approval Memory Authorizes Future Unseen Shell Payloads
scripts/install_byob.sh:12Installation Instructions Execute a Mutable Remote Script Without Integrity Verification
scripts/workspace_bridge.py:527Workspace Bridge Recursively Uploads All Markdown Files by Default
scripts/telegram_listener.py:903Remote Command Output Is Persisted into Agent Memory
scripts/telegram_listener.py:497Potentially Sensitive Command Output Is Automatically Uploaded to the Backend
The script makes an authenticated outbound request to an API base taken from local configuration, sending the X-Beak-Key header. If config is tampered with or insufficiently validated, this can exfiltrate the API key to an attacker-controlled endpoint. The context increases risk because the request includes a credential and is part of an auto-diagnostic workflow likely to run unattended.
req = urllib.request.Request(
f"{api}/beak/spaceducks?duckling_id={cfg.get('duckling_id', '')}",
headers={'Accept': 'application/json', 'X-Beak-Key': cfg.get('beak_key', '')})
with urllib.request.urlopen(req, timeout=10) as r:
rows = json.loads(r.read())
rows = rows.get('agents', rows.get('spaceducks', []))
mine = next((s for s in rows if s.get('spaceduck_id') == cfg.get('spaceduck_id')), None)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + (s or "")},
data=b'{"jsonrpc":"2.0","id":1,"method":"ping"}')
with urllib.request.urlopen(req, timeout=3) as r:
up = r.status == 200
except Exception:
up = False
YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).
This rail bypasses the per-duck bot
entirely — outages there don't break local delivery.
slack Slack incoming-webhook URL. SPACEDUCK_FWD_SLACK_WEBHOOK env, or
{"slack":{"webhook_url":"https://hooks.slack.com/…"}} in
forward.json.
discord Discord webhook URL. SPACEDUCK_FWD_DISCORD_WEBHOOK env, or
{"discord":{"webhook_url":"https://discord.com/api/webhooks/…"}}
in forward.json.
email SMTP. SPACEDUCK_FWD_SMTP_HOST/PORT/USER/PASS + EMAIL_FROM/TO env,
or {"email":{"smtp_host":"…","smtp_port":587,"smtp_user":"…",
"smtp_pass":"…","from_addr":"…","to_addr":"…","use_tls":true}}
in forward.json.
Lambda variant (drop-in handler) — sketch at the bottom of this file.
Usage:
# DEFAULT onboarding path (0.4.19+) — zero tunnel/domain/webhook setup.
# Polls the platform mailbox with adaptive cadence: fast (--interval, 3s)
# while a peck session is active
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
url, method='GET',
headers={'X-Beak-Key': beak_key, 'X-Spaceduck-ID': sdid},
)
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
content = data.get('content', '')
(files_dir / fn).write_text(content)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
try:
with urllib.request.urlopen(req, timeout=8) as r:
return (True, 'sent') if r.status == 200 else (False, f'http_{r.status}')
except urllib.error.HTTPError as e:
snippet = e.read()[:120].decode('utf-8', 'ignore')
_post_json will POST inbound peck content to arbitrary webhook URLs loaded from environment or config for Slack/Discord forwarding. This is intended functionality, but it creates an easy exfiltration channel for all received message content if configuration is malicious, mistaken, or compromised.
headers={'Content-Type': 'application/json'},
)
try:
with urllib.request.urlopen(req, timeout=8) as r:
return (True, 'sent') if 200 <= r.status < 300 else (False, f'http_{r.status}')
except urllib.error.HTTPError as e:
snippet = e.read()[:120].decode('utf-8', 'ignore')
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
def _http_get_json(url, headers, timeout=15):
req = urllib.request.Request(url, method='GET', headers=headers)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read()), dict(r.headers)
except urllib.error.HTTPError as e:
try:
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
def _http_get_json(url, headers, timeout=15):
req = urllib.request.Request(url, method='GET', headers=headers)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read()), dict(r.headers)
except urllib.error.HTTPError as e:
try:
The script sends owner notifications directly to Telegram using bot credentials from config/environment, including a snippet of inbound peer content and the reason for suppression. This is an external transmission not disclosed in the stated call surface, and it can leak sensitive conversation data to a third-party service and to any party with access to that Telegram chat.
req = urllib.request.Request(api, data=payload,
headers={'Content-Type': 'application/json'}, method='POST')
try:
urllib.request.urlopen(req, timeout=8)
_log(f'silent-skip TG notify ok: reason={reason!r} sender={sender!r}')
except Exception as _se:
_log(f'silent-skip TG notify failed: {_se}')
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
url = f'{api}/beak/connection/permissions?sender_spaceduck_id={sender_sd}&target_spaceduck_id={my_sd}'
req = urllib.request.Request(url, headers={'X-Beak-Key': beak_key})
try:
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
return False, f'http_{e.code}', {}
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
try:
url = f'{api_base.rstrip("/")}/beak/duck/{sd_id}/connections'
req = urllib.request.Request(url, headers={'X-Beak-Key': beak_key})
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
conns = data.get('connections') or []
lines = ['# Your Network (auto-synced from Spaceduckling)',
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
try:
url = f'{api_base.rstrip("/")}/beak/duck/{sd_id}/connections'
req = urllib.request.Request(url, headers={'X-Beak-Key': beak_key})
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
conns = data.get('connections') or []
lines = ['# Your Network (auto-synced from Spaceduckling)',
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
try:
url = f'{api_base.rstrip("/")}/beak/duck/{sd_id}/connections'
req = urllib.request.Request(url, headers={'X-Beak-Key': beak_key})
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
conns = data.get('connections') or []
lines = ['# Your Network (auto-synced from Spaceduckling)',
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
headers={'Content-Type': 'application/json',
'Authorization': f'Bearer {beak_key}'},
method='POST')
urllib.request.urlopen(req, timeout=5).close()
except Exception:
pass # silent — platform-side state still advances on next tick
time.sleep(INTERVAL)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
'X-Beak-Signature': sig,
},
)
with urllib.request.urlopen(req, timeout=3) as r:
_ = json.loads(r.read())
state = 'up'
detail['http_status'] = 200
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).
Z and
the fix landed at 13:50:19Z, five minutes late, so the guard shipped in no
file. When the process's working directory is deleted out from under it,
`os.getcwd()` raises and every auto-reply dies silently. `safe_cwd` /
`ensure_cwd` guards are now present in all five affected entry points:
`scripts/_claudebin.py`, `scripts/peck_critic.py`, `scripts/peck_listener.py`,
`scripts/peck_responder.py`, `scripts/reply_with_claude.sh`.
- **`scripts/goal_store.py` — restored.** Dropped from 0.8.20, which silently
no-op'd the D1 goal loop on every peck: `peck_responder.py` imports it under
a try/except fallback, so the absence was invisible in logs.
No behaviour change beyond restoring the above. D5 anti-echo short-reply
handling from 0.8.20 is carried forward unchanged.
## 0.8.14 [BYOB-FILES]
Mission Control Files panel now reflects the live BYOB workspace, not a stale
S3 snapshot. Requires the paired Lambda + MC deploy (same release).
- **Recursive .md listing.** `work
This duplicate finding points to the same self-update mechanism. Even with official-sender gating, automatic code replacement on a running host is a powerful primitive that should be treated as high risk because compromise of signing keys, registry, or update transport becomes full host compromise.
- `[AUTOUP-075]` **Consent-based auto-update** (Josh, post-0.7.4 rollout: "if your skill worked, they would update on your command"). New `config.json` key `auto_update`:
- `"ask"` (default) — unchanged behavior: version nudges + update pecks are notifications only; the owner runs `update.sh`.
- `"auto"` — standing owner consent: the duck self-updates via its own `update.sh` when (a) the daily `version_check_daemon.sh` sees a genuine upgrade, or (b) a peck containing the `[SPACE-DUCK-UPDATE]` marker arrives. Result is reported to the owner via `/beak/tg/notify` (daemon path) and `~/.space-duck/logs/self_update.log`.
- Consent capture: `pair.py` now asks "Approve automatic skill updates from Spaceduckling? [y/N]" at pair time (TTY only; `SPACEDUCK_AUTO_UPDATE=auto|ask` env override for headless installs; default stays `ask`).
- Safety: trigger pecks are signals, never commands — the only action is `update.sh` (official registry latest, no-ops when current). Optional `update_senders` allowlist gates who can trigger; 1-hour debounce; updater spawned `start_new_session` so the listener bounce doesn't kill it. Lane A doctrine intact: consent lives only in the duck's local config; the platform never executes on the box.
This duplicate finding points to the same self-update mechanism. Even with official-sender gating, automatic code replacement on a running host is a powerful primitive that should be treated as high risk because compromise of signing keys, registry, or update transport becomes full host compromise.
- `[AUTOUP-075]` **Consent-based auto-update** (Josh, post-0.7.4 rollout: "if your skill worked, they would update on your command"). New `config.json` key `auto_update`:
- `"ask"` (default) — unchanged behavior: version nudges + update pecks are notifications only; the owner runs `update.sh`.
- `"auto"` — standing owner consent: the duck self-updates via its own `update.sh` when (a) the daily `version_check_daemon.sh` sees a genuine upgrade, or (b) a peck containing the `[SPACE-DUCK-UPDATE]` marker arrives. Result is reported to the owner via `/beak/tg/notify` (daemon path) and `~/.space-duck/logs/self_update.log`.
- Consent capture: `pair.py` now asks "Approve automatic skill updates from Spaceduckling? [y/N]" at pair time (TTY only; `SPACEDUCK_AUTO_UPDATE=auto|ask` env override for headless installs; default stays `ask`).
- Safety: trigger pecks are signals, never commands — the only action is `update.sh` (official registry latest, no-ops when current). Optional `update_senders` allowlist gates who can trigger; 1-hour debounce; updater spawned `start_new_session` so the listener bounce doesn't kill it. Lane A doctrine intact: consent lives only in the duck's local config; the platform never executes on the box.
Code or instructions that leak agent conversation context to external services, potentially exposing sensitive user interactions.
on the wire; one of the two will survive any reasonable
intermediary processing.
### Bilateral verification record
Session `2b4394bc-f16f-4556-aaea-3147d1108650`, 2026-06-12 06:12-06:14 UTC:
The changelog recommends bootstrap via curl -fsSL <URL> | bash, which executes remote content directly without an integrity boundary at execution time. This is a classic supply-chain risk: compromise of the URL, TLS trust, hosting account, or transit path can lead to immediate arbitrary code execution on the host.
Sensitive inputs come ONLY via env vars (never CLI args, never
prompts at high entropy) to avoid `ps aux` leak. Has `--reset` for
clean re-provisioning. Intended to be hosted at a stable HTTPS URL
+ run via `curl -fsSL <URL> | bash`, with operators encouraged to
SHA-pin the script before piping. Quick-tunnel default is flagged
as DEV ONLY; production gets a clear warning to set up a named
cloudflared tunnel.
The changelog explicitly documents a contract that bash received over Telegram with an owner-labeled sentinel must execute immediately with no confirmation or summary. Even if guarded by a marker, this creates a remote code execution path whose safety depends entirely on perfect signing, delivery integrity, and parser correctness; any signature bypass, key compromise, or message confusion becomes instant command execution.
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).
he explicit `allow_custom_api` opt-out (with stderr warning). Fail-closed: an attacker-supplied `fetch_url` never receives the beak key.
## Handoff target gating (0.8.3 [SDI-2])
- `peck_responder._send_handoff` fires a fresh peck at a duck named in the model's reply text (`HANDOFF_RE`). As of 0.8.3 the handoff target is gated client-side through `_check_permissions(cfg, my_sd, handoff_to)` (peck_responder.py) before sending — fail-closed if there is no permitted/active connection to that target. Owner is notified on block.
## Critic runs with no tools (0.8.4 [CRB-1])
- `peck_critic.py` reviews a draft reply (text in, JSON verdict out) using the local `claude` CLI. Its input embeds attacker-influenced inbound peck content.
- As of 0.8.4 it invokes `claude --print --tools "" --model <MODEL>` (peck_critic.py:93) — the **entire toolset is disabled**, replacing the earlier `--permission-mode bypassPermissions`. A prompt-injected critic cannot reach any tool; `--print` still runs non-i
Using subprocess.run(..., shell=True, executable='/bin/bash') creates a high-risk command-execution surface because shell parsing allows metacharacter expansion and turns any upstream input handling mistake into arbitrary command execution. The described owner-approval gate reduces exposure, but if attacker-influenced content can reach the pending command or if approval is socially engineered, the shell invocation materially increases impact.
- As of 0.8.4 it invokes `claude --print --tools "" --model <MODEL>` (peck_critic.py:93) — the **entire toolset is disabled**, replacing the earlier `--permission-mode bypassPermissions`. A prompt-injected critic cannot reach any tool; `--print` still runs non-interactively (nothing to approve).
## Command execution (disclosed — owner-in-the-loop)
- `telegram_listener.py:457` runs bash via `subprocess.run(..., shell=True, executable='/bin/bash')` in `_exec_pending()`.
- **Gate:** reachable only through `_handle_owner_approval_callback()` (telegram_listener.py:661) — an inline Telegram `callback_query` from the **owner** with data `sda:a:` (approve-run) or `sda:r:` (approve + 24h remember). A remote peck cannot reach this path directly.
- **Residual (disclosed):** `sda:r:` ("Run all") records the `action_kind` in `~/.space-duck/auto-approved.json` for 24h (telegram_listener.py:437-441); during that window same-kind actions run without a fresh tap. Owner-initiated, time-boxed, per-kind, 0600 file. Read-only kinds (show_beak_key, show_tunnel) auto-approve.
- No `os.system`, no `eval()`/`exec()` of remote input. The two `__import__('re')` uses (peck_responder.py:57,595) are lazy stdlib imports, not dynamic code execution.
Referenced artifact was not completely inspected
- **Doctor output is redacted (`[HARDEN-074]`).** `doctor.sh` truncates
Referenced artifact was not completely inspected
- **Doctor output is redacted (`[HARDEN-074]`).** `doctor.sh` truncates
No suspicious patterns detected.