Skip to content

fix(gateway): fail closed on runtime config edits#70726

Merged
drobison00 merged 2 commits into
openclaw:mainfrom
drobison00:gateway-config-fail-closed
Apr 23, 2026
Merged

fix(gateway): fail closed on runtime config edits#70726
drobison00 merged 2 commits into
openclaw:mainfrom
drobison00:gateway-config-fail-closed

Conversation

@drobison00

@drobison00 drobison00 commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

fix(gateway): fail closed on runtime config edits

Summary

  • Problem: gateway config.apply / config.patch relied on a hand-maintained denylist, so agent-driven runtime edits could still mutate sensitive operator-controlled config outside the listed paths.
  • Why it matters: a non-trusted model could rewrite security-sensitive runtime config such as remote endpoints, global tool policy, exec-adjacent settings, and other operator-owned state.
  • What changed: replaced the denylist guard with a fail-closed allowlist for narrowly agent-tunable paths, computed changed config leaf paths before validation, and added regression coverage for newly reported sensitive paths plus allowed prompt/model/mention-gating edits.
  • What did NOT change (scope boundary): no .github, CI, workflow, or automation changes; no new gateway RPCs; no config schema expansion beyond the runtime guard behavior.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: the agent-facing gateway config mutation guard trusted a hand-maintained denylist of protected paths, so any sensitive subtree not explicitly listed remained writable.
  • Missing detection / guardrail: regression coverage did not fail closed on unlisted runtime config paths.
  • Contributing context (if known): config.patch applies deep merge semantics, including merge-by-id behavior for keyed arrays, which made incomplete denylist coverage especially brittle.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    src/agents/openclaw-gateway-tool.test.ts
    src/agents/tools/gateway-tool-guard-coverage.test.ts
  • Scenario the test should lock in: only a narrow allowlist of agent-tunable config paths remains mutable, while sensitive paths such as gateway.remote.url, tools.allow, memory.qmd.command, and browser.executablePath remain blocked.
  • Why this is the smallest reliable guardrail: these tests exercise the mutation guard directly at the agent-facing gateway tool boundary.
  • Existing test that already covers this (if any): the gateway tool tests already covered protected-path rejection for earlier denylist entries; this change extends that boundary to a fail-closed model.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Agent-driven gateway config.apply and config.patch now allow only the explicitly allowlisted prompt/model/mention-gating paths.
  • Other runtime config edits are rejected up front with a protected-path error instead of being persisted.

Diagram (if applicable)

Before:
[agent gateway config.patch] -> [denylist misses subtree] -> [sensitive config persisted]

After:
[agent gateway config.patch] -> [changed paths collected] -> [allowlist check] -> [reject unless explicitly safe]

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? Yes
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: the gateway tool can now mutate fewer config paths than before. This is a fail-closed reduction in surface area, with regression coverage added for newly reported sensitive paths.

Repro + Verification

Environment

  • OS: Linux 6.8.0-110-generic x86_64 GNU/Linux
  • Runtime/container: Codex workspace container
  • Model/provider: N/A
  • Integration/channel (if any): N/A
  • Relevant config (redacted): default test runtime setup

Steps

  1. Run CI=1 pnpm exec vitest run --config test/vitest/vitest.agents.config.ts src/agents/openclaw-gateway-tool.test.ts
  2. Run CI=1 pnpm exec vitest run --config .artifacts/gateway-guard-vitest.config.ts
  3. Inspect git diff --stat origin/main..origin-base-check

Expected

  • The gateway tool tests pass.
  • The guard coverage tests pass.
  • The branch diff stays limited to the three gateway guard files.

Actual

  • src/agents/openclaw-gateway-tool.test.ts: 25 tests passed.
  • src/agents/tools/gateway-tool-guard-coverage.test.ts: 36 tests passed.
  • The branch diff is limited to:
    src/agents/openclaw-gateway-tool.test.ts
    src/agents/tools/gateway-tool-guard-coverage.test.ts
    src/agents/tools/gateway-tool.ts

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: confirmed the pushed fix branch contains only the three gateway guard files, confirmed the fork-based pushed commit has the same patch identity as the upstream-based validation commit, and ran both targeted gateway guard test files successfully.
  • Edge cases checked: blocked gateway.remote.url rewrites, blocked global tools policy rewrites, and allowed prompt/model/mention-gating edits through the new allowlist.
  • What you did not verify: full-repo test matrix, upstream PR creation, or resyncing the fork's main branch to current upstream history.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? No
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps:

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk:
    • Existing workflows that relied on the agent mutating broader runtime config may now be rejected.
  • Mitigation:
    • The allowlist intentionally preserves only narrow prompt/model/mention controls; broader config changes must move through explicit operator-managed paths.

@aisle-research-bot

aisle-research-bot Bot commented Apr 23, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Agent can delete or rename configured agents via allowlisted agents.list[].id/model edits (policy bypass)
2 🟡 Medium Potential CPU/memory exhaustion in gateway config mutation path-diffing
3 🟡 Medium Allowlist check matches path prefixes, allowing nested mutations under allowlisted keys
1. 🟠 Agent can delete or rename configured agents via allowlisted agents.list[].id/model edits (policy bypass)
Property Value
Severity High
CWE CWE-284
Location src/agents/tools/gateway-tool.ts:31-236

Description

The gateway tool’s config mutation guard moved from a denylist of protected paths to an allowlist of “agent-tunable” paths. As part of this, agents.list[].id is allowlisted, and the new changed-path collector treats agents.list as a keyed array by id.

Because deletions/renames of keyed entries are represented as per-entry diffs at the synthetic path agents.list[], removing an entry (or changing its id) is reduced to changes on that entry’s leaf fields (e.g., agents.list[].id, agents.list[].model). Those leaf fields are allowlisted, so the guard will not block removal/renaming.

Impact depends on deployment, but in configs where additional agents provide safety/oversight (e.g., monitoring/policy/approval agents) and their entries only contain allowlisted fields, an untrusted model could:

  • Delete those agents from agents.list, making them unavailable for routing / enforcement.
  • Rename an agent id, causing role/routing confusion (downstream code identifies agents by normalized id).

Relevant code:

const ALLOWED_GATEWAY_CONFIG_PATHS = [// ...
  "agents.list[].id",
  "agents.list[].model",// ...
];// When a keyed array entry disappears, nextEntries.get(id) is undefined,// so collectConfigLeafPaths() records only leaf paths under `${basePath}[]`.
collectChangedConfigPaths(currentEntries.entries.get(id), nextEntries.entries.get(id), `${basePath}[]`, out);

Downstream, agent identity and availability are driven by agents.list ids:

  • listAgentIds(cfg) returns ids derived from cfg.agents.list entries; removing entries removes those agent ids from the system.

Recommendation

Fail closed on membership/identity changes to agents.list.

Options:

  1. Remove agents.list[].id from the allowlist and require operator control for renames.
  2. Forbid deletions of existing agent entries via agent-driven config.patch/config.apply (or allow only additions), by explicitly checking keyed-array id sets.
  3. Treat any keyed-array membership change as a change to the parent path (agents.list) and block unless explicitly allowlisted.

Example hardening (conceptual):

if (basePath === "agents.list") {
  const currentIds = new Set(currentEntries.entries.keys());
  const nextIds = new Set(nextEntries.entries.keys());
  if (!setsEqual(currentIds, nextIds)) {
    out.add("agents.list"); // not allowlisted => blocked
    return out;
  }
}

Also consider an explicit invariant that safety/oversight agents must be present and immutable via agent-facing tools.

2. 🟡 Potential CPU/memory exhaustion in gateway config mutation path-diffing
Property Value
Severity Medium
CWE CWE-400
Location src/agents/tools/gateway-tool.ts:277-295

Description

assertGatewayConfigMutationAllowed computes changed paths by recursively traversing the entire config and performing deep structural comparisons without any explicit bounds.

  • raw is parsed as JSON5 with no maximum size/depth limits.
  • For config.patch, the mutation is applied (applyMergePatch) and then the entire current and next configs are diffed.
  • collectChangedConfigPaths() performs isDeepStrictEqual(currentValue, nextValue) at every recursion level and, when unequal, recurses into objects/arrays and builds Sets of keys/ids.
  • Finally changedPaths is expanded into an array and sorted (toSorted()), adding extra CPU/memory pressure.

An agent (not a trusted principal per comments) that can call the owner-only gateway tool could provide a very large/deep raw object to induce high CPU usage and memory allocations, potentially blocking the gateway event loop (denial-of-service).

Vulnerable code:

const nextConfig = ...applyMergePatch(...)
const changedPaths = [...collectChangedConfigPaths(params.currentConfig, nextConfig)].toSorted();
...
function collectChangedConfigPaths(currentValue, nextValue, basePath = "", out = new Set()) {
  if (isDeepStrictEqual(currentValue, nextValue)) return out;
  ... // recursively walks objects/arrays
}

Recommendation

Add explicit resource bounds and early-exit behavior for agent-supplied config mutations.

Suggested mitigations (combine as appropriate):

  1. Cap input size for raw (and/or parsed node count/depth) before parsing/diffing.

  2. Short-circuit diffing: instead of enumerating all changed paths and sorting them, walk the parsed mutation object (the patch) and validate that only allowlisted paths are being touched; fail fast on the first disallowed path.

  3. Bound recursion by tracking depth and total visited nodes/keys and throwing an error if exceeded.

Example guard (illustrative):

const MAX_RAW_BYTES = 50_000;
if (params.raw.length > MAX_RAW_BYTES) {
  throw new Error(`gateway ${params.action} raw too large`);
}

function collectChangedConfigPaths(..., depth = 0, budget = { nodes: 0 }) {
  if (depth > 50) throw new Error("config mutation too deep");
  if (++budget.nodes > 20_000) throw new Error("config mutation too large");
  ...
}

This prevents a model/agent from supplying pathological payloads that monopolize CPU/memory.

3. 🟡 Allowlist check matches path prefixes, allowing nested mutations under allowlisted keys
Property Value
Severity Medium
CWE CWE-284
Location src/agents/tools/gateway-tool.ts:261-275

Description

isAllowedGatewayConfigPath() treats allowlist entries as prefixes (pattern length <= path length), so any descendant of an allowlisted path is also allowed.

This becomes dangerous in combination with collectChangedConfigPaths(): when a value changes type (e.g., string → object), it records only the base path as changed and does not enumerate the new object's leaf paths. If that base path is allowlisted, an attacker can submit a patch that replaces an allowlisted scalar with an object containing arbitrary nested keys, and the guard will still allow the mutation.

Impact depends on downstream config consumers, but common outcomes include:

  • Fail-open / logic bypass if code treats non-boolean values as truthy (e.g., requireMention set to an object)
  • Denial of service if code assumes a string/boolean and crashes on object access
  • Future bypass risk if additional semantics are later added under an allowlisted key.

Vulnerable code:

// Prefix match: patternSegments.length > pathSegments.length is the only length check.
if (patternSegments.length > pathSegments.length) {
  return false;
}
...
return true;

Recommendation

Make allowlist matching exact by default (same number of segments), and only allow prefix/descendant matches when explicitly requested (e.g., patterns ending with .*).

Additionally, when types differ in collectChangedConfigPaths, consider collecting leaf paths from the new value too, so nested keys cannot be smuggled under an allowlisted parent.

Example fix (exact match):

function isAllowedGatewayConfigPath(path: string): boolean {
  const pathSegments = path.split(".");
  return ALLOWED_GATEWAY_CONFIG_PATHS.some((pattern) => {
    const patternSegments = pattern.split(".");
    if (patternSegments.length !== pathSegments.length) return false;
    for (let i = 0; i < patternSegments.length; i++) {
      if (!pathSegmentMatches(patternSegments[i], pathSegments[i])) return false;
    }
    return true;
  });
}

Example fix (type-change leaf enumeration):

// when current/next are different primitive/object kinds
collectConfigLeafPaths(currentValue, basePath, out);
collectConfigLeafPaths(nextValue, basePath, out);

Analyzed PR: #70726 at commit 4a62641

Last updated on: 2026-04-23T21:23:36Z

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M maintainer Maintainer-authored PR labels Apr 23, 2026

@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: 83be9659e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/tools/gateway-tool.ts
@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR converts the gateway config-mutation guard from a denylist to a narrow allowlist: rather than blocking a fixed set of known-sensitive paths, assertGatewayConfigMutationAllowed now rejects any path that isn't explicitly listed in ALLOWED_GATEWAY_CONFIG_PATHS. The new collectChangedConfigPaths helper performs a structural diff of current vs. next config and emits leaf-level changed paths, which are then checked against the allowlist; the dangerous-flag secondary check is preserved. The allowlist approach is a clear security improvement — newly-introduced config keys are blocked by default rather than requiring manual denylist updates.

Two P2 notes: the isAllowedGatewayConfigPath pattern matcher uses prefix semantics (a 3-segment pattern matches a 5-segment path), which transitively allows arbitrary nested keys under complex-typed allowed fields (e.g. promptOverlays), relying on downstream schema validation; and the PR description describes an entirely different change (webhook secret caching) that does not match any file in this diff.

Confidence Score: 5/5

Safe to merge — no P0/P1 findings; the allowlist approach is strictly more secure than the replaced denylist.

All findings are P2 (style/documentation concerns). The core logic — structural diff + allowlist matching + dangerous-flag secondary check — is correct and the test coverage is thorough. The prefix-match permissiveness is by design and mitigated by schema validation downstream.

No files require special attention for merge safety. The PR description should be corrected before merging for traceability.

Comments Outside Diff (1)

  1. src/agents/openclaw-gateway-tool.test.ts, line 1 (link)

    P2 PR description describes a different change than what is implemented

    The PR title says "fix(gateway): fail closed on runtime config edits" but the description is entirely about fixing webhook route secret caching (createTaskFlowWebhookRequestHandler, WeakMap, extensions/webhooks/src/http.test.ts). The actual code changes are in src/agents/tools/gateway-tool.ts and convert the config-mutation guard from a denylist to an allowlist. The description mentions no files that appear in this diff and references test runs of a completely separate extension. Future reviewers and bisect tooling will find it very difficult to correlate this PR with the real change. The description should be rewritten to describe the gateway-tool allowlist migration.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/openclaw-gateway-tool.test.ts
    Line: 1
    
    Comment:
    **PR description describes a different change than what is implemented**
    
    The PR title says "fix(gateway): fail closed on runtime config edits" but the description is entirely about fixing webhook route secret caching (`createTaskFlowWebhookRequestHandler`, `WeakMap`, `extensions/webhooks/src/http.test.ts`). The actual code changes are in `src/agents/tools/gateway-tool.ts` and convert the config-mutation guard from a denylist to an allowlist. The description mentions no files that appear in this diff and references test runs of a completely separate extension. Future reviewers and bisect tooling will find it very difficult to correlate this PR with the real change. The description should be rewritten to describe the gateway-tool allowlist migration.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/tools/gateway-tool.ts
Line: 255-268

Comment:
**Prefix-match semantics allows any path nested under an allowed prefix**

`isAllowedGatewayConfigPath` returns `true` whenever a *pattern* is a prefix of the emitted *path* — it only validates the first `patternSegments.length` segments and ignores any trailing segments. That means the pattern `agents.defaults.model` also allows `agents.defaults.model.executor`, `agents.defaults.promptOverlays.someNestedKey`, etc. For scalar fields (`model`, `thinkingDefault`) this is harmless because `collectChangedConfigPaths` only emits the leaf path. For complex-typed fields like `promptOverlays`, the traversal recurses into object/array children and emits deeper paths, which all pass the allowlist check by prefix match. The guard therefore relies on downstream config-schema validation to reject unexpected object shapes at allowed paths. Consider adding a comment documenting this reliance, or switching to exact-length matching for scalar-typed allowed paths.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/openclaw-gateway-tool.test.ts
Line: 1

Comment:
**PR description describes a different change than what is implemented**

The PR title says "fix(gateway): fail closed on runtime config edits" but the description is entirely about fixing webhook route secret caching (`createTaskFlowWebhookRequestHandler`, `WeakMap`, `extensions/webhooks/src/http.test.ts`). The actual code changes are in `src/agents/tools/gateway-tool.ts` and convert the config-mutation guard from a denylist to an allowlist. The description mentions no files that appear in this diff and references test runs of a completely separate extension. Future reviewers and bisect tooling will find it very difficult to correlate this PR with the real change. The description should be rewritten to describe the gateway-tool allowlist migration.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(gateway): fail closed on runtime con..." | Re-trigger Greptile

Comment thread src/agents/tools/gateway-tool.ts
Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
@drobison00 drobison00 force-pushed the gateway-config-fail-closed branch from 83be965 to 4a62641 Compare April 23, 2026 21:20
@drobison00 drobison00 merged commit bceda60 into openclaw:main Apr 23, 2026
68 checks passed

@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: 4a626413f9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/tools/gateway-tool.ts
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* fix(gateway): fail closed on runtime config edits

* changelog + telegram topic requireMention depth

Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* fix(gateway): fail closed on runtime config edits

* changelog + telegram topic requireMention depth

Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
* fix(gateway): fail closed on runtime config edits

* changelog + telegram topic requireMention depth

Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* fix(gateway): fail closed on runtime config edits

* changelog + telegram topic requireMention depth

Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* fix(gateway): fail closed on runtime config edits

* changelog + telegram topic requireMention depth

Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* fix(gateway): fail closed on runtime config edits

* changelog + telegram topic requireMention depth

Append a user-facing Unreleased/Fixes entry describing the fail-closed
gateway config-mutation allowlist, and extend the allowlist so Telegram
topic-level paths like
channels.telegram.groups.<group>.topics.<topic>.requireMention stay
agent-tunable instead of being rejected as protected after this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant