Skip to content

fix(api): close scope-enforcer body-injection + header-target gaps (P1)#719

Merged
vasconceloscezar merged 1 commit into
devfrom
fix/scope-enforcer-header-targets-precedence
Jun 17, 2026
Merged

fix(api): close scope-enforcer body-injection + header-target gaps (P1)#719
vasconceloscezar merged 1 commit into
devfrom
fix/scope-enforcer-header-targets-precedence

Conversation

@vasconceloscezar

Copy link
Copy Markdown
Collaborator

Resolves the three P1 authorization findings from the #589 (dev → main) bot review, verified still-present on current dev.

Problem

extractLockTargets() — the helper feeding both the allowlist enforcer and require-signed-instance — resolved targets from path params + JSON body only, body-first. Two failure modes:

  1. Scope bypass (Codex P1, scope-enforcer.ts)PATCH /instances/:realId with body.instanceId = <allowlisted-id> made allowlist + require_genie_signature evaluate against the injected id, not the path target.
  2. Missed signature enforcement (Codex P1, require-signed-instance.ts) — header-scoped routes (POST /turns/close targets via x-omni-instance/x-omni-chat) returned no instance target, so the signature gate was skipped on requireGenieSignature instances.
  3. False 403 (Codex P1, scope-enforcer.ts) — same root cause: header targets were invisible, so allowlisted profile keys were wrongly denied on header-scoped routes.

Fix

  • extractLockTargets now also reads x-omni-instance / x-omni-chat (new readHeaderTargets(c) helper) and applies precedence path > header > body. Route-derived targets (path, header) are trusted; the caller-controllable body can no longer override them — it's used only when neither path nor header supplies the target (outbound sends, explicit-body turn close).
  • Both scope-enforcer and require-signed-instance middleware pass header targets through.

Tests

  • path target beats a conflicting body.instanceId/body.chatId (bypass guard)
  • x-omni-* headers populate targets for header-scoped routes
  • header beats body; path beats header; body still used when no path/header target
  • all existing extraction + enforcement + signature tests preserved · full gate green (4299 pass / 0 fail)

🤖 Generated with Claude Code

Addresses three P1 findings from the #589 bot review:

extractLockTargets resolved authorization targets from path params and the
JSON body only, with the body taking precedence. This allowed:
  • Scope bypass — a caller could hit a path-scoped write (e.g.
    PATCH /instances/:realId) and inject body.instanceId pointing at an
    allowlisted instance, so allowlist + require_genie_signature checks
    evaluated against the wrong instance.
  • Missed enforcement / false 403 — header-scoped routes (POST /turns/close
    targets via x-omni-instance / x-omni-chat) were invisible to the
    extractor, so signature enforcement was skipped and allowlisted profile
    keys were wrongly denied.

Fix:
  • extractLockTargets now also reads x-omni-instance / x-omni-chat (via the
    new readHeaderTargets helper) and applies precedence path > header > body.
    Route-derived targets (path, header) are trusted; the caller-controllable
    body can no longer override them and is used only as a last resort.
  • require-signed-instance + scope-enforcer middleware both pass header
    targets through.

Tests: path/header beat conflicting body targets; headers populate targets
for header-scoped routes; body still used when no path/header target.
@vasconceloscezar vasconceloscezar merged commit 5a8ed4e into dev Jun 17, 2026
2 of 5 checks passed
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50297181-466d-455d-8ef5-a6b99b07f8d5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/scope-enforcer-header-targets-precedence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vasconceloscezar vasconceloscezar deleted the fix/scope-enforcer-header-targets-precedence branch June 17, 2026 13:38

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces header-derived targets (x-omni-instance and x-omni-chat) to the scope enforcer middleware, ensuring that route-derived targets (path parameters and headers) take precedence over the request body to prevent scope bypass. It also adds comprehensive unit tests to verify this precedence logic. The review feedback suggests simplifying the normalization and extraction of these header targets in scope-enforcer.ts by utilizing the logical OR (||) operator to reduce verbosity and improve readability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +252 to +258
export function readHeaderTargets(c: Context<{ Variables: AppVariables }>): HeaderLockTargets {
const norm = (v: string | undefined): string | null => (v && v.length > 0 ? v : null);
return {
instance: norm(c.req.header('x-omni-instance')),
chat: norm(c.req.header('x-omni-chat')),
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The norm helper function is recreated on every call to readHeaderTargets and adds unnecessary complexity. Since c.req.header returns string | undefined, we can simplify this by using the logical OR (||) operator to default to null directly, which is cleaner and more idiomatic.

export function readHeaderTargets(c: Context<{ Variables: AppVariables }>): HeaderLockTargets {
  return {
    instance: c.req.header('x-omni-instance') || null,
    chat: c.req.header('x-omni-chat') || null,
  };
}

Comment on lines +272 to +273
const headerInstance = headers?.instance && headers.instance.length > 0 ? headers.instance : null;
const headerChat = headers?.chat && headers.chat.length > 0 ? headers.chat : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Since headers.instance and headers.chat are already normalized to string | null | undefined, we can simplify the check for non-empty strings using the logical OR (||) operator. This avoids the verbose length check and is much more readable.

Suggested change
const headerInstance = headers?.instance && headers.instance.length > 0 ? headers.instance : null;
const headerChat = headers?.chat && headers.chat.length > 0 ? headers.chat : null;
const headerInstance = headers?.instance || null;
const headerChat = headers?.chat || null;

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