Skip to content

fix(gateway): enforce allowRequestSessionKey gate on template-rendered mapping sessionKeys#69381

Merged
pgondhi987 merged 12 commits intoopenclaw:mainfrom
pgondhi987:fix/fix-481
Apr 21, 2026
Merged

fix(gateway): enforce allowRequestSessionKey gate on template-rendered mapping sessionKeys#69381
pgondhi987 merged 12 commits intoopenclaw:mainfrom
pgondhi987:fix/fix-481

Conversation

@pgondhi987
Copy link
Copy Markdown
Contributor

Summary

  • Problem: Hook mappings that use {{…}} template expressions in their sessionKey (e.g. the built-in gmail preset with sessionKey: \"hook:gmail:{{messages[0].id}}\") render attacker-controlled webhook payload values into the session key, then pass that value to resolveHookSessionKey() with source: \"mapping\". The allowRequestSessionKey gate only checked source === \"request\", so the mapping path was silently exempt.
  • Why it matters: An operator who disabled allowRequestSessionKey (the secure default) believed they were preventing external callers from steering session routing. Template-rendered mapping keys bypassed that control entirely — a hook-token holder could route the dispatched message into any session their payload value could produce.
  • What changed: HookAction gains sessionKeySource: \"static\" | \"templated\"; HookTransformResult gains the same field; resolveHookSessionKey's source type expands to \"mapping-static\" | \"mapping-templated\"; the allowRequestSessionKey gate now also fires on mapping-templated; resolveMergedSessionKeySource handles transform overrides with a safe \"templated\" default; resolveHooksConfig throws if a mapping uses a templated sessionKey without allowedSessionKeyPrefixes; the shared hasHookTemplateExpressions utility eliminates the regex duplication.
  • What did NOT change: Static operator-provided mapping keys (sessionKey: \"hook:gmail:fixed\") are unaffected and continue to bypass the gate as before.

🤖 AI-assisted PR — generated by OpenAI Codex, reviewed by Claude.

Change Type (select all)

  • Bug fix
  • Security hardening

Scope (select all touched areas)

  • Gateway / orchestration
  • API / contracts

Linked Issue/PR

  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: resolveHookSessionKey() was introduced with a source: \"request\" | \"mapping\" discriminator, but the allowRequestSessionKey gate was only wired to source === \"request\". The \"mapping\" branch renders external webhook data through renderOptional(mapping.sessionKey, ctx) before reaching that function, making the distinction security-relevant, not just semantic.
  • Missing detection / guardrail: No test covered the mapping path with allowRequestSessionKey=false; no config-time validation required allowedSessionKeyPrefixes when template expressions were present.
  • Contributing context (if known): The original mapping branch was treated as operator-controlled. Once template expressions referencing payload.* were added, that assumption no longer held.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
  • Target test or file: src/gateway/hooks.test.ts, src/gateway/hooks-mapping.test.ts
  • Scenario the test should lock in: (1) mapping-templated source is blocked by resolveHookSessionKey when allowRequestSessionKey=false; (2) mapping-static is not blocked; (3) transform-provided sessionKey without explicit sessionKeySource defaults to \"templated\"; (4) transform-provided sessionKey with sessionKeySource: \"static\" is respected; (5) resolveHooksConfig throws when a templated mapping key is present but allowedSessionKeyPrefixes is absent.
  • Why this is the smallest reliable guardrail: Each test directly exercises one decision point in the gate chain without a live gateway.
  • Existing test that already covers this (if any): None — the pre-existing mapping-path test used source: \"mapping\" which is now renamed.

User-visible / Behavior Changes

  • New config-time error: resolveHooksConfig throws \"hooks.allowedSessionKeyPrefixes is required when a hook mapping sessionKey uses templates\" if any mapping (or preset) has a template-driven sessionKey but allowedSessionKeyPrefixes is unset. Operators who use the gmail preset or write custom template-bearing session keys must now also set allowedSessionKeyPrefixes.
  • Tightened runtime gate: Mappings with template-rendered session keys now require allowRequestSessionKey: true, same as request-supplied keys. Operators who run those mappings with the default allowRequestSessionKey: false will get a 400 error until they either set allowRequestSessionKey: true or supply a static sessionKey.
  • Static mapping session keys and auto-generated session keys are unaffected.

Diagram (if applicable)

Before:
[webhook POST] -> renderOptional(sessionKey, ctx) -> resolveHookSessionKey(source:"mapping") -> gate skipped -> dispatch

After (templated key):
[webhook POST] -> renderOptional(sessionKey, ctx) -> resolveHookSessionKey(source:"mapping-templated") -> gate fires -> 400 if allowRequestSessionKey=false

After (static key):
[webhook POST] -> resolveHookSessionKey(source:"mapping-static") -> gate skipped -> dispatch (unchanged)

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux (CI), Darwin arm64 (local review)
  • Runtime/container: Node.js 22
  • Model/provider: N/A
  • Integration/channel: Gateway hooks (/hooks/<mapping>)
  • Relevant config (redacted): hooks.enabled=true, hooks.token=<secret>, hooks.presets=[\"gmail\"], hooks.allowRequestSessionKey=false

Steps

  1. Configure a gateway with hooks.presets: [\"gmail\"] and hooks.allowRequestSessionKey: false (the default).
  2. POST a crafted webhook payload to /hooks/gmail with an attacker-chosen messages[0].id.
  3. Observe the dispatched session key.

Expected

  • Gateway rejects the request with a 400 indicating session key is disabled, or requires allowedSessionKeyPrefixes to be set at startup.

Actual (before fix)

  • Gateway accepts the request and dispatches to the attacker-chosen session key.

Evidence

  • Failing test/log before + passing after — new tests in hooks.test.ts and hooks-mapping.test.ts cover all five scenarios; the source: \"mapping\"source: \"mapping-static\" rename in the existing passing test confirms the old neutral path still works.

Human Verification (required)

  • Verified scenarios: Codex-generated fix; code reviewed by Claude. All five new test cases exercise distinct gate-chain decision points. Transform-override merge logic (resolveMergedSessionKeySource) verified to default to \"templated\" when sessionKeySource is absent.
  • Edge cases checked: Transform sets static sessionKey explicitly; transform sets dynamic sessionKey without declaring source; no-transform path (returns base directly); allowedSessionKeyPrefixes both set and unset.
  • What you did not verify: Live gateway round-trip against a real Gmail Pub/Sub push endpoint.

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 — see User-visible / Behavior Changes above.
  • Config/env changes? Yes — allowedSessionKeyPrefixes is now required when template session keys are used.
  • Migration needed? Yes — operators using hooks.presets: [\"gmail\"] or custom template session keys must add allowedSessionKeyPrefixes to their hooks config. Operators who also want to retain template-driven routing must additionally set allowRequestSessionKey: true.
  • If yes, exact upgrade steps:
    1. Add hooks.allowedSessionKeyPrefixes: [\"hook:gmail:\"] (or the relevant prefix) to your hooks config.
    2. If you need template-driven session routing, also set hooks.allowRequestSessionKey: true.

Risks and Mitigations

  • Risk: Operators using the gmail preset will see a startup error until they add allowedSessionKeyPrefixes.
    • Mitigation: Error message is actionable and names the required field. Static-key mappings and auto-generated keys are unaffected.
  • Risk: Operators who legitimately rely on template-rendered session keys for routing (e.g. per-thread isolation) will additionally need allowRequestSessionKey: true.
    • Mitigation: The combination of allowedSessionKeyPrefixes + allowRequestSessionKey: true preserves the existing routing capability while constraining it to the declared prefix namespace.

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: M maintainer Maintainer-authored PR labels Apr 20, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR closes a security gap where {{…}}-templated sessionKey values in hook mappings (including the built-in Gmail preset) bypassed the allowRequestSessionKey gate because the mapping dispatch path used source: "mapping" instead of distinguishing static from attacker-influenced keys. The fix correctly threads sessionKeySource: "static" | "templated" through HookActionHookTransformResultresolveHookSessionKey, and adds a config-time guard requiring allowedSessionKeyPrefixes when any mapping carries a templated session key.

Confidence Score: 5/5

This PR is safe to merge; it is a targeted security hardening with no P0/P1 issues and strong test coverage for all new decision points.

All changes are within the gate chain and config-validation layer. The conservative default of treating any non-'static' sessionKeySource (including undefined) as 'mapping-templated' ensures unknown/missing sources are blocked, not silently allowed. Transform-injected session keys without a declared source also inherit the safe 'templated' default. Existing behaviour for static mapping keys and auto-generated keys is demonstrably unchanged. The five new test scenarios directly exercise each decision point. No P0/P1 findings.

No files require special attention.

Reviews (2): Last reviewed commit: "fix: address build failures" | Re-trigger Greptile

Comment thread src/gateway/hooks.ts Outdated
Comment thread src/gateway/hooks-mapping.ts
Copy link
Copy Markdown

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

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: 81319435d5

ℹ️ 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/gateway/server-http.ts Outdated
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Apr 20, 2026
@pgondhi987
Copy link
Copy Markdown
Contributor Author

@codex review

@pgondhi987
Copy link
Copy Markdown
Contributor Author

@greptile review

Copy link
Copy Markdown

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

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

ℹ️ 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/gateway/hooks.ts Outdated
Comment on lines +92 to +93
mappings.some((mapping) => hasTemplatedHookSessionKey(mapping.sessionKey)) &&
!allowedSessionKeyPrefixes
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 Respect mapping precedence in templated-session validation

This guard treats any templated sessionKey in the resolved mapping list as a hard config error, but applyHookMappings evaluates explicit hooks.mappings before preset mappings (src/gateway/hooks-mapping.ts), so a static explicit mapping can intentionally shadow the templated Gmail preset. In that common override pattern (presets:["gmail"] plus a static gmail mapping), startup now fails with a prefixes error even though the effective mapping is static, breaking the documented “override preset with static sessionKey” path and introducing an unnecessary boot-time regression.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in b5f7fb2.

Copy link
Copy Markdown

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

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

ℹ️ 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/gateway/hooks.ts Outdated
Comment on lines +376 to +379
if (earlier.matchPath !== mapping.matchPath) {
return false;
}
return earlier.matchSource === undefined || earlier.matchSource === mapping.matchSource;
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 Account for wildcard path shadowing in mapping precedence

hasEffectiveTemplatedHookSessionKeyMapping is meant to ignore mappings that can never run, but isHookMappingShadowed only treats mappings as shadowed when matchPath strings are equal. An earlier mapping with no match.path (wildcard) already captures all paths, so a later templated mapping is unreachable; however, this code still marks it as effective and makes resolveHooksConfig throw the new allowedSessionKeyPrefixes error. That creates a startup regression for valid configs that intentionally place a static catch-all mapping before narrower templated mappings.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 9b1a549.

Copy link
Copy Markdown

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

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: 9b1a549e34

ℹ️ 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/gateway/hooks.ts Outdated
continue;
}
effectiveMappings.push(mapping);
if (hasTemplatedHookSessionKey(mapping.sessionKey)) {
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 Ignore wake mappings in templated-session validation

hasEffectiveTemplatedHookSessionKeyMapping flags any effective mapping whose sessionKey contains {{...}}, but it does not check the mapping action. sessionKey is ignored for action: "wake" in buildActionFromMapping, so a wake-only mapping that happens to include a templated sessionKey now triggers a startup error requiring hooks.allowedSessionKeyPrefixes even though no session routing path can use that value. This is a regression for previously valid wake configs that included extra fields.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 37a7e75.

Copy link
Copy Markdown

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

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: 37a7e75607

ℹ️ 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/gateway/hooks.ts Outdated
Comment on lines +376 to +379
if (earlier.matchPath !== undefined && earlier.matchPath !== mapping.matchPath) {
return false;
}
return earlier.matchSource === undefined || earlier.matchSource === mapping.matchSource;
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 Treat empty match path/source as wildcard in shadow check

isHookMappingShadowed uses !== undefined checks, but runtime matching treats empty strings as wildcards (mappingMatches only checks truthy matchPath/matchSource). Fresh evidence: normalizeMatchPath("/") returns "", so an earlier static catch-all mapping with match.path: "/" (or match.source: "") is considered non-shadowing here, and resolveHooksConfig can incorrectly throw the new allowedSessionKeyPrefixes error for later templated mappings that are actually unreachable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 706600d.

@pgondhi987
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ 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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation gateway Gateway runtime maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant