Back to skill

Security audit

Space Duck

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a legitimate Space Duck integration skill, but its public webhook mode has an unauthenticated file-write weakness that needs review before installation.

Prefer poll mode instead of exposing the HTTP push listener. Do not run the push listener on a public interface until peck_id is strictly validated and the endpoint is protected by authentication, firewalling, or a trusted reverse proxy. Keep auto_update set to ask, avoid allow_custom_api unless you control the backend, and review workspace_bridge behavior before enabling Markdown sync to the platform.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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
Findings (1)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/peck_listener.py:184
Finding

Unauthenticated Path Traversal Enables Arbitrary JSON File Overwrite

Content
View full analysis

Vulnerability Details

File Location: scripts/peck_listener.py, lines 1–24, 184–202, 405–428, 1228–1229, and 1286
Vulnerability Type: Unauthenticated path traversal and arbitrary file overwrite
Risk Level: High

Vulnerable Code

python
"""
AUTH:   The backend identifies itself with X-SpaceDuck-Event:
        peck.received header. There is no inbound HMAC today
...
"""
python
def _fetch_shared_mds_for_peck(event, listener_cfg):
    refs = event.get('shared_mds') or []
    if not refs:
        return None
    peck_id = event.get('peck_id') or f'peck_{int(time.time()*1000)}'
    files_dir = INBOX / f'{peck_id}.files'
    try:
        files_dir.mkdir(parents=True, exist_ok=True)
python
def do_POST(self):
    if self.path != '/peck':
        self.send_response(404); self.end_headers(); return

    n = int(self.headers.get('Content-Length') or 0)
    raw = self.rfile.read(n) if n else b''
    try:
        event = json.loads(raw)
    except Exception:
        self._send_json(400, {'error': 'invalid_json'}); return

    evt_hdr = self.headers.get('X-SpaceDuck-Event', '?')
    peck_id = event.get('peck_id') or f'peck_{int(time.time()*1000)}'
    sender  = event.get('sender_name') or event.get('sender_spaceduck_id') or '?'
    msg     = (event.get('message') or '')[:140]

    # Persist to inbox before any side-effects.
    try:
        INBOX.mkdir(parents=True, exist_ok=True)
        inbox_path = INBOX / f'{peck_id}.json'
        inbox_path.write_text(json.dumps(event, indent=2))
        _align_inbox_ownership(inbox_path)
python
p.add_argument('--host', default='0.0.0.0',
               help='HTTP server bind host (push mode only)')
python
srv = ThreadingHTTPServer((args.host, args.port), PeckHandler)

Technical Analysis

The push-mode listener accepts arbitrary JSON requests at /peck without cryptographic authentication. The X-SpaceDuck-Event header is read only for display and is not validated as an aut ...[truncated 2708 chars]

Remediation
View remediation

Remediation Suggestions

  1. Strictly validate peck_id before any filesystem use. Permit only a constrained identifier format, for example:

    python
    import re
    
    if not isinstance(peck_id, str) or not re.fullmatch(
        r'[A-Za-z0-9_-]{1,128}', peck_id
    ):
        self._send_json(400, {'error': 'invalid_peck_id'})
        return
    
  2. Enforce directory containment after canonicalization. Resolve the destination and verify that it remains beneath INBOX:

    python
    inbox_root = INBOX.resolve()
    destination = (inbox_root / f'{peck_id}.json').resolve()
    
    if destination.parent != inbox_root:
        self._send_json(400, {'error': 'invalid_peck_id'})
        return
    

    Apply equivalent containment checks to files_dir and every attachment destination.

  3. Prefer locally generated storage names. Store untrusted protocol identifiers inside file contents or map them to a local random identifier instead of using them as filenames.

  4. Authenticate inbound webhooks. Require an HMAC signature covering the timestamp, nonce, path, and raw body. Enforce a short timestamp window and nonce replay protection before parsing or persisting the event.

  5. Use a safer network default. Bind push mode to 127.0.0.1 by default and require an explicit option to expose it externally.

  6. Add regression tests covering ../config, absolute paths, nested separators, encoded separators, platform-specific separators, empty identifiers, overlong identifiers, and attachment-directory traversal.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • 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 (334)

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The script performs outbound network requests to an API base derived from configuration and sends the bearer-like X-Beak-Key header to that endpoint. If an attacker can modify config.json or influence the API base, they can redirect this request to an attacker-controlled server and exfiltrate the key or probe arbitrary endpoints.

Content

Scanner excerpt · scripts/doctor_fix.py (reported line 156)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/mcp_server.py (reported line 507)May include surrounding context.

python
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 'agent_skill_credential_exfiltration_webhook': AI agent skill credential harvesting followed by webhook or external exfiltration [agent_skills]

Critical
Category
YARA Match
Confidence
85% confidence
Finding

YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Content

Scanner excerpt · scripts/peck_listener.py (reported line 38)May include surrounding context.

python
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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/peck_listener.py (reported line 231)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/peck_listener.py (reported line 293)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/peck_listener.py (reported line 310)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/peck_listener.py (reported line 597)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/peck_listener.py (reported line 615)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/peck_responder.py (reported line 294)May include surrounding context.

python
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:
            # 0.9.7 [RESP-D] LEG B — TG send failed: also buffer durably so a

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The API base URL comes from local config and is used directly to construct an authenticated HTTP request carrying the Beak key in a header. If that config is tampered with, the responder will disclose its credential to an arbitrary host and trust permission responses from that host, enabling credential exfiltration and policy bypass.

Content

Scanner excerpt · scripts/peck_responder.py (reported line 336)May include surrounding context.

python
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}', {}

Tainted flow: 'prompt' from sys.stdin.read (line 593, user input) → subprocess.run (code execution)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

External input (network, user) flows to a code execution sink. This enables remote code execution or command injection.

Content

Scanner excerpt · scripts/peck_responder.py (reported line 597)May include surrounding context.

python
'from your own host)\n' + _res +
                          '\n\nNow compose your reply using these facts. '
                          'Do not request further lookups.')
                out = subprocess.run(cmd, input=prompt, capture_output=True,
                                     text=True, timeout=DEFAULT_TIMEOUT,
                                     cwd=_cwd)
        if out.returncode != 0:

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/workspace_bridge.py (reported line 480)May include surrounding context.

python
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)',

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/workspace_bridge.py (reported line 551)May include surrounding context.

python
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)',

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/workspace_bridge.py (reported line 998)May include surrounding context.

python
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)',

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/workspace_bridge.py (reported line 634)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
97% confidence
Finding

cmd_status claims to probe the local bridge, but it constructs a URL from user-supplied --bind and performs urllib.request.urlopen to that target without constraining it to loopback beyond a narrow 0.0.0.0/:: substitution. An attacker who can influence this argument can trigger outbound requests to arbitrary hosts/ports, which is SSRF-like behavior and may also disclose the bearer key in the Authorization header to unintended local or remote services.

Content

Scanner excerpt · scripts/workspace_bridge.py (reported line 933)May include surrounding context.

python
'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 'offensive_tool_references': References to well-known offensive security tools [hacktools]

High
Category
YARA Match
Confidence
70% confidence
Finding

YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).

Content

Scanner excerpt · CHANGELOG.md (reported line 59)May include surrounding context.

md
listener.py`) declares `platform_webhook`.
  Optional field — older servers ignore it (graceful degradation).
- **[P4] `intent_gate.py` third class `security_decline`.** A security-correct
  refusal to attest a version/grant/ID is detected *before* the action scan, so
  it no longer matches verify/confirm and triggers a re-ask loop, nor falls to
  non-actionable and gets silently dropped. `peck_responder.py` notifies the
  owner exactly once and closes. Self-test corpus extended → 21/21.
- **[P5] `scripts/rail_test.py`.** Connectivity-only rail test: sends a
  neutral-wording peck carrying a random nonce (no grant/attestation/version/ID
  vocabulary — that trips peer-fraud heuristics), PASS = platform delivery ack +
  the peer's nonce echo. Never asks the peer to attest anything.

No new egress hosts. `pending_sends.json` is a local 0600 file. SECURITY-MANIFEST
updated with sections for every new file/surface.

## 0.8.21 — 2026-09-02 [FORWARD-FIX-0821]

Forward fix for two thin

Self-Modification

High
Category
Rogue Agent
Confidence
90% confidence
Finding

This duplicate finding refers to the same self-update mechanism. Because it modifies installed code and can be triggered non-interactively after initial consent, it materially increases the consequences of any trust-boundary failure.

Content

Scanner excerpt · CHANGELOG.md (reported line 487)May include surrounding context.

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

Self-Modification

High
Category
Rogue Agent
Confidence
90% confidence
Finding

This duplicate finding refers to the same self-update mechanism. Because it modifies installed code and can be triggered non-interactively after initial consent, it materially increases the consequences of any trust-boundary failure.

Content

Scanner excerpt · CHANGELOG.md (reported line 487)May include surrounding context.

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

Context Leakage

High
Category
Data Exfiltration
Confidence
85% confidence
Finding

The changelog includes a concrete session identifier and bilateral verification record from a live exchange. Even in documentation, publishing real session IDs and interaction metadata can leak operational context, enable correlation across logs/systems, and expose internal identifiers that should be treated as sensitive telemetry.

Content

Scanner excerpt · CHANGELOG.md (reported line 1450)May include surrounding context.

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

External Script Fetching

High
Category
Supply Chain
Confidence
96% confidence
Finding

The changelog explicitly recommends a curl -fsSL <URL> | bash installation path. Piping remote content directly into a shell is dangerous because it executes unreviewed network-fetched code immediately and turns any compromise of the hosting location, TLS trust, or distribution path into arbitrary code execution.

Content

Scanner excerpt · CHANGELOG.md (reported line 1489)May include surrounding context.

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

YARA rule 'offensive_tool_references': References to well-known offensive security tools [hacktools]

High
Category
YARA Match
Confidence
70% confidence
Finding

YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).

Content

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

md
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

YARA rule 'offensive_tool_references': References to well-known offensive security tools [hacktools]

High
Category
YARA Match
Confidence
70% confidence
Finding

YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).

Content

Scanner excerpt · scripts/setup_listeners_supervised.sh (reported line 50)May include surrounding context.

sh
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

Tool Parameter Abuse

High
Category
Tool Misuse
Confidence
80% confidence
Finding

The manifest discloses use of subprocess.run(..., shell=True, executable='/bin/bash'), which is inherently dangerous because shell parsing can turn attacker-controlled or insufficiently validated input into arbitrary command execution. The owner-approval gate lowers exposure, but if approval UI, action construction, or upstream content is influenced by an attacker, shell=True materially increases blast radius.

Content

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

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

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- **Doctor output is redacted (`[HARDEN-074]`).** `doctor.sh` truncates

Static analysis

No suspicious patterns detected.