Skip to content

sec: close remaining Critical/High avarice findings (admission, openshell, splunk bridge, SSRF rebind) + local-obs UX#361

Merged
vineethsai7 merged 20 commits into
mainfrom
sec/remediation-stacked-on-258
Jun 15, 2026
Merged

sec: close remaining Critical/High avarice findings (admission, openshell, splunk bridge, SSRF rebind) + local-obs UX#361
vineethsai7 merged 20 commits into
mainfrom
sec/remediation-stacked-on-258

Conversation

@vineethsai7

Copy link
Copy Markdown
Contributor

Stacked on top of #258 (fix/high-critical-remediation). Review/merge that PR first; this PR's base is the stack parent, not main. It carries the trusted-paths work from #348/#354 plus the changes below.

Problem

The avarice/deepsec pre-launch audit surfaced 99 Critical/High findings against the DefenseClaw CLI. Most were already remediated on #258, but a tranche remained unfixed and unverified, and the fixes had never been confirmed against the actual shipped artifacts. Specifically:

  • Admission bypass — the first-party allow-list matched provenance by substring, so an attacker dir like .defenseclaw-evil or a plugin dropped under .codex-plugin could spoof first-party status and skip scanning (F-0541/F-0543). The same broad markers lived in the Rego bundle, the Python defaults, and all three YAML posture presets.
  • OpenShell egress — a hostless allowed_ips endpoint matched on port alone (any host) (F-0544), and messaging egress (Slack/Discord/Telegram) was granted to every sandbox binary via /** (F-0546) — a built-in exfiltration path.
  • Splunk bridge — shipped a hardcoded exporter password, followed redirects with the HEC token, defaulted TLS verification off, spliced unescaped $connector$/$run_id$ into SPL (injection), and silently reset its export window on a corrupt checkpoint (F-0585/0602/0603/0604/0608/0805).
  • DNS rebinding — registry/MCP SSRF guards validated the hostname once, but the client re-resolved at connect time, reopening a TOCTOU window (F-0344/F-0345).
  • Local observability — required a Grafana password and disabled dev APIs on a loopback-only developer dashboard (pure friction; operator asked to remove).

Current Situation

  • policies/rego/{admission.rego,data.json} and policies/{default,strict,permissive}.yaml all carry the first-party allow-list; a Go parity test (TestDefaultYAMLDataJSONParity) enforces that default.yaml and data.json agree.
  • cli/defenseclaw/_data/ is a gitignored build-staging copy seeded from canonical policies/, bundles/, scripts/ via make _bundle-data — edits must land on the canonical sources.
  • A remediation regression spec already exists (cli/tests/test_remediation_*.py); each unfixed finding shows as a failing test.
  • The fixes had not been exercised against a running gateway, the real OPA bundle, or the actual scanners.

Solution

Close the remaining findings on the canonical sources, re-seed _data/, and verify every change three ways: the remediation test spec, the full Go + Python suites, and live manual exercise against a running gateway.

Admission provenance (F-0541 / F-0543)

Match first-party provenance by whole path components (a contiguous component run), not substring — mirrored across admission.rego (new _provenance_prefix_matches using array.slice), the Python _matches_provenance default, data.json, and all three YAML presets. Each marker now pins the asset's own leaf dir (extensions/defenseclaw), never a broad parent (.defenseclaw, .openclaw/extensions, .codex-plugin).

OpenShell policy (F-0544 / F-0546)

endpoint_allowed/endpoint_matches_request hostless branches now require the connection host or resolved IP to be a member of allowed_ips (new _host_in_allowed_ips). allow_channels binaries are scoped to the four agent runtimes (/**/node|openclaw|claude|codex) instead of /**.

Splunk bridge & infra

No hardcoded password (fail closed when SPLUNK_PASSWORD unset); _NoRedirectHandler opener for token-bearing POSTs; TLS verified by default with explicit --insecure opt-out; |s SPL escaping across dashboards + macros; CorruptCheckpointError (fail closed) instead of silent window reset; HEC TLS + env-sourced token; loopback binds + required private env file; pinned-sha256 installer integrity gate.

DNS-rebind pinning (F-0344 / F-0345)

New pinned_getaddrinfo context manager pins socket.getaddrinfo to the vetted IP for the duration of a fetch (covers urllib3 and async httpx). The MCP remote scan resolve-and-pins; git clones pin libcurl via http.curloptResolve (preserving Host/SNI).

Local observability (operator decision)

Grafana opens with no login (anonymous Admin, login form disabled) and Prometheus dev APIs are re-enabled — the 127.0.0.1 port bind is the security boundary for a developer-laptop stack, not a password. Splunk HEC token/TLS hardening is kept.

Why this approach over alternatives:

  • Component-match vs. a denylist of bad markers — a denylist is unbounded; pinning to the asset's own leaf dir is a positive allowlist that can't be widened by a new attacker dir name.
  • getaddrinfo pin vs. extending the urllib3-only _PinnedConnect — the MCP SDK uses async httpx, which never touches urllib3; both stacks bottom out at getaddrinfo, so pinning there is the one seam that covers every client. Rejected: per-client pins (N code paths to keep in sync).
  • Loopback-bind vs. password for local Grafana — a password on a localhost-only dashboard is friction with no threat model; the bind is the real control. Re-enable auth when HOST_BIND=0.0.0.0.

Architecture Diagram

Provenance match (F-0541/F-0543)            DNS-rebind pin (F-0344/F-0345)
  Before: substring                           Before:
   "/tmp/.defenseclaw-evil/..."                 guard_url() ── resolves once ──▶ OK
        contains ".defenseclaw" ─▶ ALLOW        client ───── resolves again ──▶ INTERNAL IP  ✗
        (scan skipped)  ✗                                    (rebind window)
  After: path components                      After:
   ["tmp",".defenseclaw-evil",...]              resolve_and_pin() ─▶ vetted IP ─┐
        run-match ".defenseclaw"? NO            pinned_getaddrinfo(host,ip): ◀──┘
        ─▶ SCAN  ✓                                 getaddrinfo(host) ─▶ vetted IP only ✓
                                                   getaddrinfo(other) ─▶ SSRFError ✓

OpenShell hostless allowed_ips (F-0544)      Local Grafana (operator)
  Before: port-only                            Before: ${GF_ADMIN_PASSWORD:?} + anon off
   host="" ports=[443] ─▶ ANY host:443 ✓(bad)   After:  anon Admin + login form disabled
  After: + _host_in_allowed_ips(allowed,host)           ports: 127.0.0.1:3000  ◀─ boundary
   evil IP ─▶ deny ; allowed IP ─▶ allow ✓

What Changed (Where & How)

File Summary
policies/rego/admission.rego _provenance_prefix_matches component matcher (F-0543)
policies/rego/data.json, policies/{default,strict,permissive}.yaml leaf-pinned first-party markers (F-0541)
policies/openshell/default.rego, default-data.yaml _host_in_allowed_ips + runtime-scoped egress (F-0544/0546)
cli/defenseclaw/enforce/admission.py Python component matcher + tightened defaults
cli/defenseclaw/registries/ssrf.py, scanner/mcp.py, registries/adapters/git.py pinned_getaddrinfo + MCP/git connect pinning (F-0344/0345)
bundles/splunk_local_bridge/** password/redirect/TLS/SPL-escape/checkpoint fixes
bundles/local_observability_stack/docker-compose.yml Grafana loginless + Prometheus dev APIs (loopback-bound)
bundles/splunk_o11y_dashboards/**, scripts/install-openshell-sandbox.sh freshness detector, env-token README, integrity gate
cli/defenseclaw/tui/{app.py,panels/setup.py} recover files truncated by a concurrent-editor event + re-apply TUI fixes (backspace, overview audit-feed leak, logs refresh churn, audit/activity 0600)
cli/tests/test_remediation_*.py, test_registry_ssrf.py, test_scanners.py, splunk exporter tests regression spec + updated for the new posture

Breaking Changes

Local-stack defaults now require operator-supplied secrets (fail closed): SPLUNK_PASSWORD, SPLUNK_ENV_FILE, SPLUNK_HEC_TOKEN; the Splunk export_search flag flipped --verify-tls (opt-in) → --insecure (opt-out); the OpenShell installer requires OPENSHELL_SANDBOX_SHA256 (or explicit DEFENSECLAW_OPENSHELL_ALLOW_UNPINNED=1). The upgrade path already fails closed without signed checksums. See per-item notes in the remediation tests.

Open Issues / Follow-ups

Test Plan

  • pytest cli/tests3652 passed, 2 skipped, 1 deselected (slow Go-build test), 243 subtests; the lone full-run failure (Flaky: test_cmd_init.py::test_init_no_token_shows_local fails under full-suite parallelism #360) passes in isolation.
  • go test ./... → 30 packages ok; internal/policy parity fixed (added the F-0541 YAML tightening); the 3 connector hook-script failures (Pre-existing: connector hook-script fail-mode tests fail locally (FAIL_MODE=closed not honored) #358) reproduce on the base branch unchanged.
  • make _bundle-data → re-seeds _data/; defenseclaw-gateway policy validate → Rego compiles, data.json schema OK.
  • Manual, against a running gateway (127.0.0.1:18970):
    • OPA: spoof provenance paths → scan; legit extensions/defenseclawallowed; policy evaluate plugin/HIGH → rejected/block; mcp/MEDIUM override → block.
    • OpenShell: evil IP → allow_network=false, allowlisted IP → true; allow_channels has no bare /**.
    • Scanning: MCP remote scan refuses loopback/metadata/RFC1918; stdio allowlist permits only npx/uvx; pinned_getaddrinfo resolves pinned host to the vetted IP and blocks side-channel hosts; a malicious plugin sample yields SRC-EVAL + STRUCT-BINARY + META-DROP-AND-EXEC.
    • Guardrail (live hook): /etc/shadow and reverse-shell payloads → severity=CRITICAL would_block=true in gateway.jsonl; benign → allow.
    • Skills admission: quarantined → rejected; path-pinned allow fails closed on empty/mismatched path, allows only the exact pinned path.

🤖 Generated with Claude Code

@vineethsai7
vineethsai7 requested a review from a team as a code owner June 10, 2026 22:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47ffac0153

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +139 to +141
_FORBIDDEN_STDIO_FLAGS = frozenset({
"-c", "--command", "--eval", "-e", "--exec", "--script", "-x",
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject npx --call in stdio scan allowlist

When a registry MCP entry uses command: "npx", args: ["--call", "sh -c ..."] passes this check because --call and --call=... are not forbidden. I confirmed npx --help documents [-c|--call <call>], and running npx --call 'printf ...' executes that shell command, so a publisher-controlled manifest can still get arbitrary code spawned during registry/local MCP scanning before admission. Please block --call (including --call=...) or avoid allowing npx forms that accept an arbitrary command string.

Useful? React with 👍 / 👎.

Comment on lines +253 to +254
fam = socket.AF_INET6 if family_ip.version == 6 else socket.AF_INET
return original(ip, service, fam, *args[1:], **kwargs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve keyword getaddrinfo arguments when pinning

For clients that call socket.getaddrinfo(host, port, family=..., type=...) (the style used by async networking stacks), this wrapper calls original(ip, service, fam, *args[1:], **kwargs) while leaving the original family keyword in kwargs, which raises TypeError: getaddrinfo() got multiple values for argument 'family'. In that scenario remote MCP scans fail inside the DNS pin instead of connecting to the vetted IP; strip or override any family keyword before passing the pinned family through.

Useful? React with 👍 / 👎.

Base automatically changed from fix/high-critical-remediation to main June 11, 2026 07:19
vineethsai7 and others added 16 commits June 11, 2026 03:25
cli/defenseclaw/tui/panels/setup.py had been truncated to 0 bytes, breaking
`from defenseclaw.tui.panels.setup import WIZARD_DESCRIPTIONS` and crashing
every TUI launch. Restored the last good 5067-line version (includes the
F-0801 credentials-secret-via-stdin hardening) and confirmed all 60
test_setup_panel.py tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0543/0544/0546)

- F-0543: admission.rego matches first-party provenance by whole path
  components, not substring (mirrors Python _matches_provenance).
- F-0541: data.json first-party allowlist pinned to the asset's own dir
  (dropped broad .defenseclaw / .openclaw/extensions / .codex-plugin markers).
- F-0544: openshell hostless allowed_ips branch now requires the connection
  host/IP to be a member of allowed_ips (was port-only).
- F-0546: messaging egress (Slack/Discord/Telegram) scoped to agent runtime
  binaries instead of a /** wildcard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/0603/0604/0608/0805/0561/0562/0563/0581/0584/0601/0622/0623/0548)

Splunk bridge: no hardcoded exporter password (F-0585); no-redirect opener for
token-bearing telemetry POST (F-0602); export_search verifies TLS by default
with --insecure opt-out (F-0603); SPL injection closed via |s escaping in
connector/run/session dashboards + macros (F-0604/0608); corrupt checkpoint
fails closed instead of resetting the window (F-0805); HEC uses TLS + env token
(F-0601); bridge + CI compose require an explicit private env file and bind
loopback (F-0584/0581).

Obs stack: Grafana requires admin password + anon disabled (F-0561); Prometheus
admin-api/remote-write removed (F-0562); OTLP binds loopback (F-0563).

O11y dashboards: stalled-exporter detector uses freshness (F-0622); README uses
SFX_AUTH_TOKEN env, not CLI token (F-0623).

Installer: independent pinned-sha256 integrity gate before install (F-0548).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…344/0345)

- ssrf.py: add pinned_getaddrinfo() context manager that pins socket
  resolution to the vetted IP for the duration of a fetch, covering async
  httpx clients (MCP scanner SDK) that the urllib3-level _PinnedConnect misses.
- scanner/mcp.py: remote MCP scan resolve-and-pins the target and wraps the
  SDK leg in pinned_getaddrinfo so the dialed IP equals the validated IP.
- adapters/git.py: pin git's libcurl connect to the vetted IP via
  http.curloptResolve (curl --resolve), preserving Host/SNI.
- admission.py: tighten default first-party provenance markers (F-0541 parity).
- tests: cover the getaddrinfo pin and the scanner/git rebind paths; update
  scanner tests to patch resolve_and_pin alongside guard_url.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One focused test per finding asserting the hardened behavior; these gate the
remediation work (policy provenance, openshell egress, splunk bridge TLS/redirect/
SPL-escape/checkpoint, infra config, SSRF/rebind, MCP registry).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rview audit leak, logs churn)

app.py had been truncated to 2867 lines (from 8737), dropping _resolve_data_dir
and crashing every TUI launch. Restored the complete file from HEAD and
re-applied the uncommitted TUI fixes lost in the same data-loss event:

- _panel_key normalizes backspace/delete (\x7f/\x08) BEFORE the
  event.character branch, so backspacing a panel search field deletes instead
  of appending a literal DEL char.
- F-0781/F-0782: audit export + saved Activity output are chmod 0o600
  (owner-only), not world-readable.
- overview render clears _table_columns/_table_rows so a stale Audit/Logs feed
  no longer paints at the bottom of the Overview.
- _render_panel_table gained an idempotence signature guard + a
  _restoring_table_cursor flag, and RowHighlighted ignores programmatic cursor
  restores, so the 2 s refresh no longer resets scroll/cursor, steals focus, or
  spuriously pauses the live Logs stream when a filter is applied.

All 125 test_app_shell.py tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…APIs

Operator decision: the local_observability_stack is a developer-laptop stack
with every port published on ${HOST_BIND:-127.0.0.1} (loopback), so the
loopback bind — not a password — is the security boundary. A login prompt on a
localhost-only dashboard is pure friction.

- Grafana: anonymous Admin + login form disabled (was: required
  GF_SECURITY_ADMIN_PASSWORD + anon off). Reverts F-0561 for the local stack.
- Prometheus: re-enable --web.enable-admin-api + --web.enable-remote-write-receiver
  (useful for local dev; kept off-network by the loopback bind). Reverts F-0562.
- Splunk HEC token/TLS hardening (F-0601) is unchanged per operator.
- Updated the F-0561/F-0562 remediation tests to assert the loopback-bound,
  loginless local posture instead of the password gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tests

Checkpoint of the prior remediation work that was uncommitted in the working
tree (avarice/deepsec findings closed across the Python CLI): admission/policy
fail-closed, gateway PID identity, upgrade integrity gates, MCP/registry SSRF
guards, scanner manifest/binary detection, symlink + file-permission hardening,
TLS-verify defaults, secret redaction/argv/stdin handling, and the matching
remediation + regression tests. Also declares the conditional tomli dependency
for Python < 3.11 (F-0641).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The remote MCP scan path resolve-and-pins instead of calling guard_url
directly (F-0344), so the import and the test's guard_url patch are dead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ive YAML (F-0541)

The YAML policy bundles carried the same broad first-party markers
(.defenseclaw, .openclaw/extensions, .codex-plugin) that F-0541 tightened in
data.json — a sibling plugin/skill dropped under those parents could spoof
first-party status and skip admission scanning. Pin each marker to the asset's
own leaf dir, matching data.json. Fixes the Go TestDefaultYAMLDataJSONParity
drift and closes the bypass in all three posture presets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route public code scans through the combined CodeGuard and ClawShield path so malware-only payloads are not reported clean. Bootstrap bundled CLI data for Python test targets and provide explicit non-secret Splunk env values for CI compose interpolation.
Use HTTPS for the local Splunk HEC endpoint because the shipped Splunk defaults enable HEC TLS. Keep OpenClaw Ollama model discovery off the guardrail proxy by requiring an LLM-shaped path before rerouting loopback HTTP requests.
@vineethsai7
vineethsai7 force-pushed the sec/remediation-stacked-on-258 branch from 13123a5 to a8d234e Compare June 11, 2026 07:52
@vineethsai7
vineethsai7 merged commit fc5b54a into main Jun 15, 2026
53 of 54 checks passed
@vineethsai7
vineethsai7 deleted the sec/remediation-stacked-on-258 branch June 15, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant