Skip to content

Security: deferred bot/harness/MAX-dispatch hardening (audit follow-ups) #108

@devswha

Description

@devswha

Context

Tracker for security audit findings deferred from the 2026-05 review pass. Closed audit P1s for the user-facing CLI surface in #106 + #107:

  • PII / real-name / personal-domain exposure
  • .env world-readable Discord token
  • SSRF guard in validateBaseURL (IMDS / RFC 1918)
  • --api-key argv leak (CWE-214) → added --api-key-file / PATINA_API_KEY_FILE
  • systemd unit hardening directives (template)
  • Discord allowlist secure-by-default (RUNTIME_ENFORCE_ALLOWLIST flipped to true)

Items in this issue affect the bot subsystems (ops/component-bridge.mjs, ops/harness.sh, marketing runtime, patina-max) and only matter when those paths are actively used. Maintainer doesn't currently run them, so the items are tracked here to be addressed when (a) the maintainer reactivates the bot, (b) a fork starts using them, or (c) someone wants a clean security posture for future contributors.

Severity ratings combine the two-reviewer audit (Claude security-reviewer + Codex via omc ask).

P1 — High (act before reactivating bot subsystems)

ops/component-bridge.mjs: Discord bot prompt-injection trust boundary

  • Files: ops/component-bridge.mjs:168, ops/runtime-bootstrap.sh:152, ops/runtime-bootstrap.sh:159
  • allowBots=true, requireMention=false, allowlist is opt-in. A compromised or low-trust bot in the configured channel can prompt-inject the patina runtime via component messages.
  • Fix: per-bot-id allowlist for forwarded messages; strip control characters from extracted payloads before injecting into the runtime prompt.

ops/marketing-runtime-bootstrap.sh: marketing runtime not isolated from primary auth

  • Files: ops/marketing-runtime-bootstrap.sh:134, 166, 181
  • Bootstrap copies auth-profiles.json, models.json, and auth/models from the primary OpenClaw config into the marketing profile. A marketing-channel prompt injection therefore acts with primary-account auth material.
  • Fix: derive a separate auth profile rather than copying primary; or document the trust assumption explicitly.

patina-max/SKILL.md: MAX Codex dispatch lacks the standalone backend sandbox

  • Files: patina-max/SKILL.md:230, 262
  • Standalone backend uses mkdtempSync + --sandbox read-only + stdin. MAX docs run codex exec without -C or sandbox; a malicious custom pattern/profile/input can become an agent prompt with repo/home access.
  • Fix: align MAX dispatch with the standalone backend's sandbox setup.

P2 — Medium

patina-max/SKILL.md: Gemini MAX dispatch via argv

  • Files: patina-max/SKILL.md:200, 256, .patina.default.yaml:64
  • Docs say all providers receive stdin, but Gemini uses gemini -p "$(cat file)". Sensitive input lands in /proc/<pid>/cmdline.
  • Fix: use stdin for Gemini, matching the documented contract.

ops/harness.sh: gh pr create --title with unsanitized LLM-produced string

  • Files: ops/harness.sh:479, 483
  • pr_title is parsed from $GENERATOR_RESULT. A manipulated title could include markdown that triggers downstream automation, or @-mentions, or escape characters.
  • Fix: length-cap to ≤120 chars and strip control characters before passing to gh.

ops/harness.sh: git apply of LLM-generated diff (supply-chain risk)

  • Files: ops/harness.sh:451-453
  • Diff is applied to a real git tree, then committed and pushed. A prompt-injection against the planner/generator (via gh issue list output at ops/harness.sh:226) could land a backdoored postinstall hook in package.json, a .github/workflows/ workflow, or a bin/ script.
  • Fix: path allowlist (deny .github/, package.json scripts, bin/, install.sh); require human review before gh pr merge --auto. AUTO_MERGE=false default mitigates today.

Bootstrap scripts echo Discord channel/guild IDs

  • Files: ops/marketing-runtime-bootstrap.sh:339-342, ops/runtime-bootstrap.sh:189-191
  • Channel IDs alone aren't credentials, but combined with a leaked bot token they let an attacker locate the targeted guild without needing OAuth introspection.
  • Fix: print only the agent name; gate channel/guild echoes behind --verbose.

src/security.js: applyInsecureBaseURLOptIn mutates process.env

  • Files: src/security.js:53-63
  • Sticky env mutation leaks across calls if patina is ever embedded as a library.
  • Fix: pass the flag explicitly through validateBaseURL (already accepts { allowInsecure }); remove the env-mutation path.

P3 — Low / Info

ops/runtime-bootstrap.sh:91-94 and ops/marketing-runtime-bootstrap.sh:235-267: Discord token via python3 argv

  • Token lands in /proc/<pid>/cmdline for the python process lifetime. Same-user ps window can read it.
  • Fix: pass via stdin or env var consumed inside the python heredoc.

ops/component-bridge.mjs:204, 210 and ops/harness.sh:189: large prompts via argv to runtime CLI

  • Local same-user process inspection can see Discord/component messages or assembled bot prompts.
  • Fix: pipe via stdin where the runtime CLI supports it.

src/api.js:42-43: errorText echoed verbatim in thrown error

  • Theoretical: a misconfigured proxy reflecting Authorization header in its error body would surface the token in stderr.
  • Fix: truncate errorText to ~512 chars and redact Bearer\s+\S+ substrings.

ops/component-bridge.mjs:11-35: .env parser doesn't decode escapes inside quoted values

  • Functionally fine for Discord tokens (alphanumeric); document the constraint or switch to dotenv.

Out of scope (won't fix unless requested)

Git history rewrite (git filter-repo)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions