feat(security): Add HTTP API security hooks for plugin scanning#6099
feat(security): Add HTTP API security hooks for plugin scanning#6099masterfung wants to merge 1 commit intoopenclaw:mainfrom
Conversation
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>
| if (hookRunner) { | ||
| try { | ||
| const preHook = await hookRunner.runHttpToolInvoke( | ||
| { | ||
| toolName, | ||
| toolParams: toolArgs, | ||
| content: JSON.stringify(toolArgs), | ||
| }, | ||
| httpCtx, |
There was a problem hiding this 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).
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.| 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, | ||
| ); |
There was a problem hiding this 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).
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.| // ====================================================================== | ||
| // 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; | ||
| } |
There was a problem hiding this 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.
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.| /** | ||
| * 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"); |
There was a problem hiding this 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.
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.
Additional Comments (1)
In This behavior can cause surprising precedence when some hooks intentionally return Prompt To Fix With AIThis 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 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. |
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:
/v1/chat/completions/v1/responses/tools/invokeAny application using the API directly (SDKs, curl) has zero protection against:
Solution
Added 4 new plugin hooks:
http_request_receivedhttp_response_sendinghttp_tool_invokehttp_tool_resultSecurity Features
Changes
src/plugins/types.tssrc/plugins/hooks.tssrc/gateway/openai-http.ts/v1/chat/completionssrc/gateway/openresponses-http.ts/v1/responsessrc/gateway/tools-invoke-http.ts/tools/invokesrc/plugins/http-hooks.test.tsTotal: 651 lines added, 9 lines removed
Test Plan
npx vitest run src/plugins/http-hooks.test.ts # ✓ 13 tests passUsage Example
Security plugins can now register for HTTP hooks:
Known Limitations
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/invokeso 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 honorblockand 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
Context used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)