Back to skill

Security audit

Space Duck

Security checks across malware telemetry and agentic risk

Overview

The skill is mostly coherent for Space Duck networking, but it bundles broad persistent background services, workspace-content syncing, and owner-approved shell execution that deserve careful review before install.

Install only if you want a Space Duck agent with persistent listeners, Telegram/BYOB integration, MCP tool bridges, and Mission Control access to local Markdown workspace files. Review config defaults before starting supervised services or workspace_bridge.py, avoid exposing unauthenticated webhook listeners publicly, keep Beak Key and Telegram/MCP secrets tightly scoped, and prefer explicit opt-in for auto-update, shell hooks, and workspace snapshot syncing.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (116)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
start_new_session=True, cwd=str(SCRIPTS),
                  env={**os.environ, 'HOME': str(HOME)})
    if isinstance(argv_or_sh, str):
        return subprocess.Popen(['/bin/sh', '-c', argv_or_sh], **kwargs)
    return subprocess.Popen(argv_or_sh, **kwargs)
Confidence
92% confidence
Finding
return subprocess.Popen(['/bin/sh', '-c', argv_or_sh], **kwargs)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"HOME": os.environ.get("HOME", "/tmp"),
               "LANG": os.environ.get("LANG", "C.UTF-8")}
        env.update(extra_env or {})
        self.proc = subprocess.Popen(
            command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL, env=env)
        self._id = 0
Confidence
89% confidence
Finding
self.proc = subprocess.Popen( command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
logf.write(f'\n== self-update triggered by peck from {sender or "?"} '
                   f'at {time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} ==\n')
        logf.flush()
        subprocess.Popen(['bash', str(update_sh)], stdout=logf, stderr=logf,
                         start_new_session=True)
        print(f'   ⬆️ auto_update=auto — self-update started (log: {log_path})')
    except Exception as e:
Confidence
83% confidence
Finding
subprocess.Popen(['bash', str(update_sh)], stdout=logf, stderr=logf, start_new_session=True)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if on_peck_cmd:
        try:
            argv = shlex.split(on_peck_cmd)
            subprocess.Popen(
                argv,
                stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
Confidence
93% confidence
Finding
subprocess.Popen( argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).communicate(input=json.dumps(ev

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# argv is parsed with shlex; subprocess runs without shell
                # interpretation. Gated upstream by --allow-shell-hook.
                argv = shlex.split(self.on_peck_cmd)
                subprocess.Popen(
                    argv,
                    stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
Confidence
93% confidence
Finding
subprocess.Popen( argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ).communicate

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = ['claude', '--print', '--append-system-prompt', _sys_framing,
           '--model', DEFAULT_MODEL]
    try:
        out = subprocess.run(cmd, input=prompt, capture_output=True,
                             text=True, timeout=DEFAULT_TIMEOUT)
        if out.returncode != 0:
            # 0.4.0 — surface BOTH stderr and stdout (claude CLI sometimes
Confidence
88% confidence
Finding
out = subprocess.run(cmd, input=prompt, capture_output=True, text=True, timeout=DEFAULT_TIMEOUT)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
'--message', handoff_text,
           '--purpose', 'handoff']
    try:
        out = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if out.returncode != 0:
            _log(f'handoff send_peck exit {out.returncode}: {out.stderr[:300]}')
            return False
Confidence
84% confidence
Finding
out = subprocess.run(cmd, capture_output=True, text=True, timeout=30)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
done downstream by the callback handler so the full text is
    available for upload to /beak/byob/workspace/action-log."""
    try:
        proc = subprocess.run(
            pending['bash'], shell=True, executable='/bin/bash',
            capture_output=True, timeout=60)
        out = (proc.stdout or b'').decode('utf-8', errors='replace')
Confidence
95% confidence
Finding
proc = subprocess.run( pending['bash'], shell=True, executable='/bin/bash', capture_output=True, timeout=60)

Tainted flow: 'kwargs' from os.environ.get (line 94, credential/environment) → subprocess.Popen (code execution)

Medium
Category
Data Flow
Content
start_new_session=True, cwd=str(SCRIPTS),
                  env={**os.environ, 'HOME': str(HOME)})
    if isinstance(argv_or_sh, str):
        return subprocess.Popen(['/bin/sh', '-c', argv_or_sh], **kwargs)
    return subprocess.Popen(argv_or_sh, **kwargs)
Confidence
95% confidence
Finding
return subprocess.Popen(['/bin/sh', '-c', argv_or_sh], **kwargs)

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

Critical
Category
Data Flow
Content
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)
Confidence
91% confidence
Finding
with urllib.request.urlopen(req, timeout=10) as r:

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

Critical
Category
Data Flow
Content
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')
Confidence
84% confidence
Finding
with urllib.request.urlopen(req, timeout=8) as r:

Tainted flow: 'LOG_PATH' from os.environ.get (line 52, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
line = f'[{time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}] {msg}\n'
    try:
        LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        with open(LOG_PATH, 'a') as f:
            f.write(line)
    except Exception:
        pass
Confidence
90% confidence
Finding
with open(LOG_PATH, 'a') as f:

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

Critical
Category
Data Flow
Content
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}')
Confidence
97% confidence
Finding
urllib.request.urlopen(req, timeout=8)

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

Critical
Category
Data Flow
Content
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)',
Confidence
94% confidence
Finding
with urllib.request.urlopen(req, timeout=10) as r:

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

Critical
Category
Data Flow
Content
headers={'Content-Type': 'application/json',
                     'Authorization': f'Bearer {beak_key}'},
            method='POST')
        with urllib.request.urlopen(req, timeout=10) as r:
            return r.status == 200
    except Exception:
        return False
Confidence
97% confidence
Finding
with urllib.request.urlopen(req, timeout=10) as r:

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

Critical
Category
Data Flow
Content
'Authorization': f'Bearer {bk}'},
                method='POST',
            )
            with urllib.request.urlopen(req, timeout=10) as r:
                print(f'# reported: HTTP {r.status}', file=sys.stderr)
        except Exception as e:
            print(f'# warn: report failed: {e}', file=sys.stderr)
Confidence
95% confidence
Finding
with urllib.request.urlopen(req, timeout=10) as r:

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

Critical
Category
Data Flow
Content
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)
Confidence
94% confidence
Finding
urllib.request.urlopen(req, timeout=5).close()

Intent-Code Divergence

Medium
Confidence
99% confidence
Finding
The docstring says the code contacts no third-party hosts directly, but `_notify_owner_silent_skip` posts to `api.telegram.org`. Misrepresenting external communications is dangerous in an autonomous agent skill because it hides a real exfiltration path from reviewers and operators, reducing informed consent and undermining trust boundaries.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The skill contains an owner-notification capability that sends received-message excerpts and metadata to Telegram, which is not necessary for core reply composition and materially broadens the data exposure surface. In a messaging agent, side-channel forwarding is especially risky because it can leak private conversations and operational states to an external platform and to any holder of the bot token/chat mapping.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The installer provisions two additional long-running background programs, capability_pulse and version_check, that perform periodic networked activity unrelated to the narrow task of keeping listeners alive. Expanding a setup script's behavior into persistent outbound communications increases attack surface, creates unexpected persistence, and can surprise operators who intended only local process supervision.

Intent-Code Divergence

Medium
Confidence
93% confidence
Finding
The docstring states that backend requests authenticate with the Beak Key, but the `/beak/duck/{sdid}/capabilities` request is sent without the `X-Beak-Key` header. This creates an unauthenticated identity-scoped call that may expose `last_seen` or other capability metadata if the backend does not enforce authorization separately, and it also indicates a mismatch between documented and actual security behavior.

Intent-Code Divergence

High
Confidence
99% confidence
Finding
The script uses `source "$STATE_FILE"` to load JWT, API URL, and PIDs from a file in `/tmp`, which executes arbitrary shell code contained in that file rather than merely parsing key/value data. Because `/tmp` is a shared, attacker-influenced location and the filename is derived from user input, a local attacker who can create or modify that state file can achieve arbitrary code execution in the context of whoever runs the teardown script.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The script's documented safety guarantee says it will never auto-restart a service that was not already running, but later logic explicitly starts the listener when no listeners are detected. This can cause unintended service activation after an update, which may expose network-facing behavior, consume resources, or change system state in ways the operator did not authorize.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
The bridge includes remote synchronization and status-reporting capabilities beyond a minimal workspace file bridge, broadening its permissions and data flows. In a security-sensitive local bridge, undeclared secondary channels materially increase the risk of silent data exfiltration and operator surprise.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
Server startup automatically launches background threads for self-pulse, snapshot upload, and connection syncing, creating undeclared ongoing outbound communications and local side effects. This is dangerous because simply starting the bridge initiates persistent telemetry and data replication unrelated to the immediate HTTP service contract.

VirusTotal

VirusTotal findings are pending for this skill version.

View on VirusTotal

Static analysis

No suspicious patterns detected.