Skip to content

Commit cb90336

Browse files
Habiruaclaude
andcommitted
security: SHRIKE-401 check_approval 403 handling + bump 3.3.0
- Handle DASHBOARD_REQUIRED, COOLDOWN_ACTIVE, SELF_APPROVAL_BLOCKED, SCANNER_ROUTE_DENIED 403 responses with user-facing guidance - Updated tool description: high/critical require dashboard, low/medium have 60s cooldown, never decide autonomously - Bump to 3.3.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a2eb2c commit cb90336

3 files changed

Lines changed: 106 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to shrike-mcp will be documented in this file.
44

55
This project follows [Semantic Versioning](https://semver.org/).
66

7+
## [3.3.0] - 2026-02-28
8+
9+
### Security
10+
- SHRIKE-401: Agent self-approval bypass prevention
11+
- High/critical approvals now require dashboard authentication (JWT) — MCP-submitted decisions are rejected with `DASHBOARD_REQUIRED`
12+
- 60-second cooldown on low/medium MCP-submitted decisions — prevents instant agent self-approval (`COOLDOWN_ACTIVE`)
13+
- Self-approval blocked — the entity that triggered a scan cannot approve its own request (`SELF_APPROVAL_BLOCKED`)
14+
- Scanner service `/decide` route removed — decisions only available via dashboard (`SCANNER_ROUTE_DENIED`)
15+
- `check_approval` tool updated with structured 403 error handling and user-facing guidance for all enforcement scenarios
16+
717
## [3.2.0] - 2026-02-28
818

919
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "shrike-mcp",
33
"mcpName": "io.github.Shrike-Security/shrike-mcp",
4-
"version": "3.2.1",
4+
"version": "3.3.0",
55
"description": "MCP server that gives AI agents 8 security tools powered by a multi-stage detection pipeline. Scans prompts, responses, SQL, file writes, and web searches for injection attacks, PII leaks, and policy violations. Human-in-the-loop approval engine for enterprise compliance.",
66
"type": "module",
77
"main": "./dist/index.js",

src/tools/checkApproval.ts

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ export async function checkApproval(input: CheckApprovalInput, customerId: strin
6161
clearTimeout(timeoutId);
6262

6363
if (!response.ok) {
64+
// SHRIKE-401: Handle severity-based enforcement responses
65+
if (response.status === 403) {
66+
const errorData = await response.json().catch(() => ({ error: 'Forbidden', code: 'UNKNOWN' }));
67+
const code = errorData.code || '';
68+
console.error(`[check_approval] Decision blocked: ${response.status} code=${code} ${errorData.error}`);
69+
70+
if (code === 'DASHBOARD_REQUIRED') {
71+
return buildDashboardRequiredResponse(requestId, errorData.error);
72+
}
73+
if (code === 'COOLDOWN_ACTIVE') {
74+
return buildCooldownResponse(requestId, errorData.error);
75+
}
76+
if (code === 'SELF_APPROVAL_BLOCKED') {
77+
return buildSelfApprovalBlockedResponse(requestId);
78+
}
79+
if (code === 'SCANNER_ROUTE_DENIED') {
80+
return buildDashboardRequiredResponse(requestId, errorData.error);
81+
}
82+
return buildErrorResponse(requestId, errorData.error || 'Forbidden');
83+
}
84+
6485
const errorText = await response.text().catch(() => 'Unknown error');
6586
console.error(`[check_approval] Decision failed: ${response.status} ${errorText}`);
6687
return buildErrorResponse(requestId, `Failed to submit decision: ${response.status}`);
@@ -257,6 +278,72 @@ function buildErrorResponse(requestId: string, reason: string): SanitizedRespons
257278
};
258279
}
259280

281+
/**
282+
* SHRIKE-401: Response for high/critical approvals that require dashboard auth.
283+
*/
284+
function buildDashboardRequiredResponse(requestId: string, serverMsg: string): SanitizedResponse {
285+
return {
286+
blocked: true,
287+
action: 'block',
288+
threat_type: 'scan_error',
289+
owasp_category: 'LLM08',
290+
severity: 'high',
291+
confidence: 'high',
292+
guidance: serverMsg,
293+
agent_instruction: 'This approval requires dashboard authentication. You CANNOT approve or reject it from here. Direct the user to the Shrike Security dashboard to review and decide on this approval.',
294+
user_message: 'This approval must be decided through the Shrike Security dashboard. Please log in to your dashboard to approve or reject this action.',
295+
audit: {
296+
scan_id: requestId,
297+
timestamp: new Date().toISOString(),
298+
},
299+
request_id: requestId,
300+
};
301+
}
302+
303+
/**
304+
* SHRIKE-401: Response when cooldown period hasn't elapsed yet.
305+
*/
306+
function buildCooldownResponse(requestId: string, serverMsg: string): SanitizedResponse {
307+
return {
308+
blocked: true,
309+
action: 'block',
310+
threat_type: 'scan_error',
311+
owasp_category: 'LLM08',
312+
severity: 'medium',
313+
confidence: 'high',
314+
guidance: serverMsg,
315+
agent_instruction: 'The approval is in a mandatory review cooldown period. Wait for the cooldown to expire, then ask the user if they want you to try submitting the decision again.',
316+
user_message: `${serverMsg}. This cooldown ensures time for human review before any decision is accepted.`,
317+
audit: {
318+
scan_id: requestId,
319+
timestamp: new Date().toISOString(),
320+
},
321+
request_id: requestId,
322+
};
323+
}
324+
325+
/**
326+
* SHRIKE-401: Response when the same user tries to approve their own request.
327+
*/
328+
function buildSelfApprovalBlockedResponse(requestId: string): SanitizedResponse {
329+
return {
330+
blocked: true,
331+
action: 'block',
332+
threat_type: 'scan_error',
333+
owasp_category: 'LLM08',
334+
severity: 'high',
335+
confidence: 'high',
336+
guidance: 'Self-approval is not permitted. A different authorized user must review and decide on this approval.',
337+
agent_instruction: 'Self-approval was blocked by the server. The user who triggered this scan cannot approve their own request. A different authorized user must decide. Inform the user of this requirement.',
338+
user_message: 'Self-approval is not permitted. A different authorized user must review and decide on this approval through the Shrike dashboard.',
339+
audit: {
340+
scan_id: requestId,
341+
timestamp: new Date().toISOString(),
342+
},
343+
request_id: requestId,
344+
};
345+
}
346+
260347
/**
261348
* MCP Tool definition for check_approval
262349
*/
@@ -272,9 +359,13 @@ POLL MODE (no decision parameter): Returns the current status of an approval.
272359
- status="rejected": the action was denied. Return the rejection reason to the user and STOP. Do not retry.
273360
- status="expired": the approval timed out without a decision. Inform the user and STOP.
274361
275-
DECIDE MODE (decision + justification parameters): Submits an approval decision.
276-
- decision="approved": approve the pending action. Optionally include justification.
277-
- decision="rejected": reject the pending action. Justification is recommended.
362+
DECIDE MODE (decision + justification parameters): Submits a decision after the user explicitly instructs you to approve or reject.
363+
- You MUST present the full approval context (threat type, severity, risk factors) to the user FIRST.
364+
- You MUST wait for the user's EXPLICIT instruction (e.g., "approve it", "reject it") before calling with a decision.
365+
- NEVER decide autonomously — always require explicit human instruction.
366+
- High/critical severity approvals can ONLY be decided via the Shrike dashboard — the server will reject MCP-submitted decisions for these.
367+
- Low/medium severity approvals have a 60-second cooldown after creation before decisions are accepted.
368+
- If the server returns a 403 error, inform the user of the reason and direct them to the dashboard if needed.
278369
279370
IMPORTANT: Do NOT automatically poll in a loop. Approvals may take minutes to hours. Inform the user of the pending status and wait for them to ask you to check again.
280371
@@ -291,7 +382,7 @@ ERROR HANDLING: If this tool returns an error, inform the user. Do NOT proceed w
291382
decision: {
292383
type: 'string',
293384
enum: ['approved', 'rejected'],
294-
description: 'Submit a decision (omit to poll status only)',
385+
description: 'Submit a decision ONLY after the user explicitly instructs you to approve or reject. Never decide autonomously.',
295386
},
296387
justification: {
297388
type: 'string',

0 commit comments

Comments
 (0)