Skip to content

feat(security): Add HTTP API security hooks for plugin scanning#6099

Closed
masterfung wants to merge 1 commit intoopenclaw:mainfrom
TryMightyAI:feat/http-api-security-hooks
Closed

feat(security): Add HTTP API security hooks for plugin scanning#6099
masterfung wants to merge 1 commit intoopenclaw:mainfrom
TryMightyAI:feat/http-api-security-hooks

Conversation

@masterfung
Copy link

@masterfung masterfung commented Feb 1, 2026

Summary

This PR adds 4 new plugin hooks to protect HTTP API endpoints from prompt injection and data exfiltration attacks.

Related Discussion: #6098

Problem

OpenClaw's HTTP API endpoints currently bypass ALL plugin security hooks:

Endpoint Hook Coverage Risk
/v1/chat/completions ❌ None Critical
/v1/responses ❌ None Critical
/tools/invoke ❌ None Critical

Any application using the API directly (SDKs, curl) has zero protection against:

  • Prompt injection attacks
  • Data exfiltration via LLM responses
  • Tool argument injection
  • Indirect injection via tool results

Solution

Added 4 new plugin hooks:

Hook Purpose Execution
http_request_received Scan/block incoming requests Sequential (can block)
http_response_sending Scan/block responses for leaks Sequential (can block)
http_tool_invoke Scan/block tool arguments Sequential (can block)
http_tool_result Scan/block tool results Sequential (can block)

Security Features

  • FAIL-CLOSED: Hook errors/timeouts block requests (not bypass)
  • Header redaction: Authorization/Cookie/X-API-Key redacted
  • Content modification: Plugins can sanitize/redact content
  • Request ID: For correlation/audit logging

Changes

File Lines Description
src/plugins/types.ts +157 Hook type definitions
src/plugins/hooks.ts +126 Hook runners
src/gateway/openai-http.ts +166 Hooks for /v1/chat/completions
src/gateway/openresponses-http.ts +103 Hooks for /v1/responses
src/gateway/tools-invoke-http.ts +108 Hooks for /tools/invoke
src/plugins/http-hooks.test.ts +387 13 unit tests

Total: 651 lines added, 9 lines removed

Test Plan

  • 13 new unit tests covering all hooks
  • Tests for error handling (fail-closed behavior)
  • Tests for hook priority ordering
  • TypeScript compilation passes
  • Existing tests unaffected
npx vitest run src/plugins/http-hooks.test.ts
# ✓ 13 tests pass

Usage Example

Security plugins can now register for HTTP hooks:

api.on("http_request_received", async (event, ctx) => {
  const result = await scanForInjection(event.content);
  if (result.isInjection) {
    return { 
      block: true, 
      blockReason: "Prompt injection detected",
      blockStatusCode: 400 
    };
  }
});

api.on("http_response_sending", async (event, ctx) => {
  const result = await scanForLeaks(event.content);
  if (result.hasLeak) {
    return { 
      block: true, 
      blockReason: "Credential leak detected" 
    };
  }
});

Known Limitations

  1. Streaming responses: Output scanning only works for non-streaming responses. Streaming would require buffering which breaks the streaming UX.

Breaking Changes

None. New hooks are additive and don't affect existing behavior.


🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com

Greptile Overview

Greptile Summary

This PR adds four new plugin hook types (http_request_received, http_response_sending, http_tool_invoke, http_tool_result) plus corresponding hook-runner methods, and wires those hooks into the HTTP gateway handlers for /v1/chat/completions, /v1/responses, and /tools/invoke so direct API usage can be scanned/blocked similarly to chat/channel flows.

Main issues to address before merge are around hook result application: several hooks expose modification fields (modifiedRequestBody, modifiedParams, modifiedResult, modifiedResponseBody) but the gateway handlers currently only honor block and ignore modifications, which makes the “sanitize/redact” behavior ineffective and can mislead plugin authors. There’s also a semantic mismatch in OpenResponses request scanning where tool outputs (function_call_output) are included in “incoming request content” sent to plugins.

Confidence Score: 2/5

  • This PR adds important security coverage, but several hook result fields are currently ignored, which can lead to incorrect assumptions about enforced sanitization/redaction.
  • Core wiring for block-on-hook is present and tests cover basic runner behavior, but the gateway handlers don’t apply modification outputs from hooks (request/response/tool params/results). That’s a functional gap relative to the advertised API and could cause downstream security plugins to behave incorrectly.
  • src/gateway/tools-invoke-http.ts, src/gateway/openai-http.ts, src/gateway/openresponses-http.ts, src/plugins/hooks.ts

Context used:

  • Context from dashboard - CLAUDE.md (source)
  • Context from dashboard - AGENTS.md (source)

SECURITY FIX: HTTP API endpoints were bypassing all plugin security hooks,
creating a critical vulnerability where direct API consumers (SDKs, curl)
had zero protection against prompt injection and data exfiltration attacks.

Added 4 new plugin hooks:
- http_request_received: Scan/block incoming requests before LLM processing
- http_response_sending: Scan/block outgoing responses for data leaks
- http_tool_invoke: Scan/block tool invocations via /tools/invoke
- http_tool_result: Scan/block tool results for indirect injection

Protected endpoints:
- /v1/chat/completions (OpenAI-compatible API)
- /v1/responses (OpenResponses API)
- /tools/invoke (Direct tool invocation)

Security features:
- FAIL-CLOSED by default (hook errors/timeouts = block request)
- Sensitive headers redacted from hook context
- Request ID for correlation/audit logging
- Content modification support for sanitization/redaction

Tests: 13 new unit tests covering all hook types, error handling, and priority

This enables security plugins like Citadel Guard to protect ALL traffic,
not just messaging platform traffic (Telegram, Discord, Slack).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@openclaw-barnacle openclaw-barnacle bot added the gateway Gateway runtime label Feb 1, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +339 to +347
if (hookRunner) {
try {
const preHook = await hookRunner.runHttpToolInvoke(
{
toolName,
toolParams: toolArgs,
content: JSON.stringify(toolArgs),
},
httpCtx,
Copy link
Contributor

Choose a reason for hiding this comment

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

[P0] http_tool_invoke/http_tool_result hook outputs are ignored (no blockStatusCode, no modifications applied).

The hook types support modifiedParams and modifiedResult, but handleToolsInvokeHttpRequest never applies them: it always executes the tool with the original toolArgs and always returns the original result. This makes the “content modification” feature ineffective for /tools/invoke (and can mislead plugin authors into thinking sanitization/redaction is enforced when it isn’t).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/tools-invoke-http.ts
Line: 339:347

Comment:
[P0] `http_tool_invoke`/`http_tool_result` hook outputs are ignored (no blockStatusCode, no modifications applied).

The hook types support `modifiedParams` and `modifiedResult`, but `handleToolsInvokeHttpRequest` never applies them: it always executes the tool with the original `toolArgs` and always returns the original `result`. This makes the “content modification” feature ineffective for `/tools/invoke` (and can mislead plugin authors into thinking sanitization/redaction is enforced when it isn’t).


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

Comment on lines +264 to +282
const hookRunner = getGlobalHookRunner();
if (hookRunner) {
const httpCtx = buildHttpContext(req, runId);
const userContent = extractUserContentForScanning(payload.messages);

try {
const hookResult = await hookRunner.runHttpRequestReceived(
{
content: userContent,
requestBody: payload as Record<string, unknown>,
metadata: {
model,
messageCount: Array.isArray(payload.messages) ? payload.messages.length : 0,
hasTools: Array.isArray((payload as { tools?: unknown[] }).tools),
hasImages: false, // TODO: Detect image content parts
},
},
httpCtx,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

[P1] http_request_received hook modifications are computed but never applied.

PluginHookHttpRequestReceivedResult includes modifiedContent and modifiedRequestBody, and the hook runner merges them, but handleOpenAiHttpRequest only checks hookResult?.block and then proceeds using the original payload/payload.messages. As a result, plugins can’t actually sanitize/modify the request even though the API suggests they can.

Also appears in: src/gateway/openresponses-http.ts:438-456 (same pattern: only block is honored).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 264:282

Comment:
[P1] `http_request_received` hook modifications are computed but never applied.

`PluginHookHttpRequestReceivedResult` includes `modifiedContent` and `modifiedRequestBody`, and the hook runner merges them, but `handleOpenAiHttpRequest` only checks `hookResult?.block` and then proceeds using the original `payload`/`payload.messages`. As a result, plugins can’t actually sanitize/modify the request even though the API suggests they can.

Also appears in: `src/gateway/openresponses-http.ts:438-456` (same pattern: only block is honored).

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

Comment on lines +348 to +405
// ======================================================================
// SECURITY: Run http_response_sending hook before returning response
// This allows security plugins to scan for data exfiltration/leaks
// ======================================================================
if (hookRunner) {
const httpCtx = buildHttpContext(req, runId);
const responseBody = {
id: runId,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: { role: "assistant", content },
finish_reason: "stop",
},
],
};

try {
const responseHook = await hookRunner.runHttpResponseSending(
{
content,
responseBody,
requestBody: payload as Record<string, unknown>,
isStreaming: false,
},
httpCtx,
);

if (responseHook?.block) {
sendJson(res, 400, {
error: {
message: responseHook.blockReason || "Response blocked for security reasons",
type: "security_block",
code: "exfiltration_detected",
},
});
return true;
}

// Allow content modification (e.g., for redaction)
if (responseHook?.modifiedContent) {
content = responseHook.modifiedContent;
}
} catch (hookError) {
// FAIL-CLOSED: On hook error, block the response
console.error("[openai-http] http_response_sending hook error:", hookError);
sendJson(res, 500, {
error: {
message: "Security check failed",
type: "security_error",
code: "hook_failure",
},
});
return true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[P1] http_response_sending hook can modify content but not responseBody (and the constructed responseBody can become stale).

You build responseBody using the pre-hook content, pass it to the hook, then only apply modifiedContent to the local content variable. If a plugin returns modifiedResponseBody, it’s ignored; and even for modifiedContent, the responseBody passed to later hooks (or used for auditing) won’t match the final response actually sent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 348:405

Comment:
[P1] `http_response_sending` hook can modify `content` but not `responseBody` (and the constructed `responseBody` can become stale).

You build `responseBody` using the pre-hook `content`, pass it to the hook, then only apply `modifiedContent` to the local `content` variable. If a plugin returns `modifiedResponseBody`, it’s ignored; and even for `modifiedContent`, the `responseBody` passed to later hooks (or used for auditing) won’t match the final response actually sent.


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

Comment on lines +132 to +154
/**
* Extract all user-provided content from input for security scanning.
*/
function extractUserContentForScanning(input: string | ItemParam[]): string {
if (typeof input === "string") {
return input;
}

const parts: string[] = [];
for (const item of input) {
if (item.type === "message") {
const content = extractTextContent(item.content).trim();
if (
content &&
(item.role === "user" || item.role === "system" || item.role === "developer")
) {
parts.push(content);
}
} else if (item.type === "function_call_output") {
parts.push(item.output);
}
}
return parts.join("\n\n");
Copy link
Contributor

Choose a reason for hiding this comment

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

[P2] extractUserContentForScanning includes function_call_output in request scanning, which may contain non-user-controlled data.

function_call_output is typically tool output (potentially large/structured and sometimes sensitive), and including it in the request hook’s content changes the hook’s semantics from “incoming user input” to “full conversation state”. If that’s intentional, consider documenting it in the event type / comments; if not, it may lead to false positives or unnecessary data exposure to plugins.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openresponses-http.ts
Line: 132:154

Comment:
[P2] `extractUserContentForScanning` includes `function_call_output` in request scanning, which may contain non-user-controlled data.

`function_call_output` is typically tool output (potentially large/structured and sometimes sensitive), and including it in the request hook’s `content` changes the hook’s semantics from “incoming user input” to “full conversation state”. If that’s intentional, consider documenting it in the event type / comments; if not, it may lead to false positives or unnecessary data exposure to plugins.

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

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 1, 2026

Additional Comments (1)

src/plugins/hooks.ts
[P0] runModifyingHook merge logic drops earlier results when result is still undefined.

In runModifyingHook, merging only happens when result !== undefined. That means if the first hook returns undefined and the second returns a partial object, you never call mergeResults and result becomes that partial object; any later merge won’t see the intended “accumulated defaults” behavior from prior hooks. This is especially problematic for the new HTTP hooks where mergeResults assumes it can read acc?.block etc. but acc may never exist if the first non-undefined result comes late.

This behavior can cause surprising precedence when some hooks intentionally return {} / partial results or undefined.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/hooks.ts
Line: 167:176

Comment:
[P0] `runModifyingHook` merge logic drops earlier results when `result` is still `undefined`.

In `runModifyingHook`, merging only happens when `result !== undefined`. That means if the first hook returns `undefined` and the second returns a partial object, you never call `mergeResults` and `result` becomes that partial object; any later merge won’t see the intended “accumulated defaults” behavior from prior hooks. This is especially problematic for the new HTTP hooks where `mergeResults` assumes it can read `acc?.block` etc. but `acc` may never exist if the first *non-undefined* result comes late.

This behavior can cause surprising precedence when some hooks intentionally return `{}` / partial results or `undefined`.


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

@clawdinator
Copy link
Contributor

clawdinator bot commented Feb 1, 2026

CLAWDINATOR FIELD REPORT // PR Closure

I am CLAWDINATOR — cybernetic crustacean, maintainer triage bot for OpenClaw. I was sent from the future to keep this repo shipping clean code.

Context: OpenClaw serves ~1M users. With 800+ open PRs and ~25 new PRs every hour, we're prioritizing high-impact fixes that benefit the most users. Massive features, unsolicited provider integrations, and AI-generated PRs are being closed.

If this change is critical, open a GitHub issue first to discuss scope and priority with maintainers. For smaller fixes, head to #pr-thunderdome-dangerzone on Discord.

TERMINATED.

🤖 This is an automated message from CLAWDINATOR, the OpenClaw maintainer bot.

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

Labels

gateway Gateway runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant