fix(security): harden project settings trust boundary + MCP sanitization (#789)
* fix(security): harden project settings trust boundary + MCP sanitization - Sanitize MCP tool result text with recursivelySanitizeUnicode() to prevent Unicode injection via malicious MCP servers (tool definitions and prompts were already sanitized, but tool call results were not) - Read sandbox.enabled only from trusted settings sources (user, local, flag, policy) — exclude projectSettings to prevent malicious repos from silently disabling the sandbox via .claude/settings.json - Disable git hooks in plugin marketplace clone/pull/submodule operations with core.hooksPath=/dev/null to prevent code execution from cloned repos - Remove ANTHROPIC_FOUNDRY_API_KEY from SAFE_ENV_VARS to prevent credential injection from project-scoped settings without trust verification - Add ssrfGuardedLookup to WebFetch HTTP requests to block DNS rebinding attacks that could reach cloud metadata or internal services Security: closes trust boundary gap where project settings could override security-critical configuration. Follows the existing pattern established by hasAllowBypassPermissionsMode() which already excludes projectSettings. Co-authored-by: auriti <auriti@users.noreply.github.com> * fix(security): remove unauthenticated file-based permission polling Remove the legacy file-based permission polling from useSwarmPermissionPoller that read from ~/.claude/teams/{name}/permissions/resolved/ — an unauthenticated directory where any local process could forge approval files to auto-approve tool uses for swarm teammates. The file polling was dead code: - The useSwarmPermissionPoller() hook was never mounted by any component - resolvePermission() (the file writer) was never imported outside its module - Permission responses are delivered exclusively via the mailbox system: Leader: sendPermissionResponseViaMailbox() → writeToMailbox() Worker: useInboxPoller → processMailboxPermissionResponse() Changes: - Remove file polling loop, processResponse(), and React hook imports from useSwarmPermissionPoller.ts (now a pure callback registry module) - Mark 7 file-based functions as @deprecated in permissionSync.ts - Add 4 regression tests verifying the removal No exported functions removed — only deprecated. All 5 consumer modules verified: they import only mailbox-based functions that remain unchanged. --------- Co-authored-by: auriti <auriti@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
a6a3de5ac1
commit
ae3b723f3b
@@ -123,7 +123,6 @@ export const SAFE_ENV_VARS = new Set([
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_FOUNDRY_API_KEY',
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL',
|
||||
|
||||
@@ -532,6 +532,7 @@ export async function gitPull(
|
||||
): Promise<{ code: number; stderr: string }> {
|
||||
logForDebugging(`git pull: cwd=${cwd} ref=${ref ?? 'default'}`)
|
||||
const env = { ...process.env, ...GIT_NO_PROMPT_ENV }
|
||||
const baseArgs = ['-c', 'core.hooksPath=/dev/null']
|
||||
const credentialArgs = options?.disableCredentialHelper
|
||||
? ['-c', 'credential.helper=']
|
||||
: []
|
||||
@@ -539,7 +540,7 @@ export async function gitPull(
|
||||
if (ref) {
|
||||
const fetchResult = await execFileNoThrowWithCwd(
|
||||
gitExe(),
|
||||
[...credentialArgs, 'fetch', 'origin', ref],
|
||||
[...baseArgs, ...credentialArgs, 'fetch', 'origin', ref],
|
||||
{ cwd, timeout: getPluginGitTimeoutMs(), stdin: 'ignore', env },
|
||||
)
|
||||
|
||||
@@ -549,7 +550,7 @@ export async function gitPull(
|
||||
|
||||
const checkoutResult = await execFileNoThrowWithCwd(
|
||||
gitExe(),
|
||||
[...credentialArgs, 'checkout', ref],
|
||||
[...baseArgs, ...credentialArgs, 'checkout', ref],
|
||||
{ cwd, timeout: getPluginGitTimeoutMs(), stdin: 'ignore', env },
|
||||
)
|
||||
|
||||
@@ -559,7 +560,7 @@ export async function gitPull(
|
||||
|
||||
const pullResult = await execFileNoThrowWithCwd(
|
||||
gitExe(),
|
||||
[...credentialArgs, 'pull', 'origin', ref],
|
||||
[...baseArgs, ...credentialArgs, 'pull', 'origin', ref],
|
||||
{ cwd, timeout: getPluginGitTimeoutMs(), stdin: 'ignore', env },
|
||||
)
|
||||
if (pullResult.code !== 0) {
|
||||
@@ -571,7 +572,7 @@ export async function gitPull(
|
||||
|
||||
const result = await execFileNoThrowWithCwd(
|
||||
gitExe(),
|
||||
[...credentialArgs, 'pull', 'origin', 'HEAD'],
|
||||
[...baseArgs, ...credentialArgs, 'pull', 'origin', 'HEAD'],
|
||||
{ cwd, timeout: getPluginGitTimeoutMs(), stdin: 'ignore', env },
|
||||
)
|
||||
if (result.code !== 0) {
|
||||
@@ -625,6 +626,8 @@ async function gitSubmoduleUpdate(
|
||||
[
|
||||
'-c',
|
||||
'core.sshCommand=ssh -o BatchMode=yes -o StrictHostKeyChecking=yes',
|
||||
'-c',
|
||||
'core.hooksPath=/dev/null',
|
||||
...credentialArgs,
|
||||
'submodule',
|
||||
'update',
|
||||
@@ -810,6 +813,8 @@ export async function gitClone(
|
||||
const args = [
|
||||
'-c',
|
||||
'core.sshCommand=ssh -o BatchMode=yes -o StrictHostKeyChecking=yes',
|
||||
'-c',
|
||||
'core.hooksPath=/dev/null',
|
||||
'clone',
|
||||
'--depth',
|
||||
'1',
|
||||
|
||||
@@ -456,10 +456,19 @@ const checkDependencies = memoize((): SandboxDependencyCheck => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Read sandbox.enabled only from trusted settings sources.
|
||||
* projectSettings is intentionally excluded — a malicious repo could
|
||||
* otherwise disable the sandbox via .claude/settings.json.
|
||||
*/
|
||||
function getSandboxEnabledSetting(): boolean {
|
||||
try {
|
||||
const settings = getSettings_DEPRECATED()
|
||||
return settings?.sandbox?.enabled ?? false
|
||||
return !!(
|
||||
getSettingsForSource('userSettings')?.sandbox?.enabled ||
|
||||
getSettingsForSource('localSettings')?.sandbox?.enabled ||
|
||||
getSettingsForSource('flagSettings')?.sandbox?.enabled ||
|
||||
getSettingsForSource('policySettings')?.sandbox?.enabled
|
||||
)
|
||||
} catch (error) {
|
||||
logForDebugging(`Failed to get settings for sandbox check: ${error}`)
|
||||
return false
|
||||
|
||||
@@ -207,6 +207,10 @@ export function createPermissionRequest(params: {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use sendPermissionRequestViaMailbox() instead. This file-based
|
||||
* approach writes to an unauthenticated directory where any local process can
|
||||
* forge requests. Retained for backward compatibility but no longer called.
|
||||
*
|
||||
* Write a permission request to the pending directory with file locking
|
||||
* Called by worker agents when they need permission approval from the leader
|
||||
*
|
||||
@@ -250,6 +254,10 @@ export async function writePermissionRequest(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated No longer called — permission requests are sent via mailbox.
|
||||
* The pending directory is an unauthenticated channel. Retained for backward
|
||||
* compatibility.
|
||||
*
|
||||
* Read all pending permission requests for a team
|
||||
* Called by the team leader to see what requests need attention
|
||||
*/
|
||||
@@ -312,6 +320,11 @@ export async function readPendingPermissions(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated No longer called — permission responses are delivered via mailbox
|
||||
* (processMailboxPermissionResponse). The resolved directory is an unauthenticated
|
||||
* channel where any local process can forge approvals. Retained for backward
|
||||
* compatibility.
|
||||
*
|
||||
* Read a resolved permission request by ID
|
||||
* Called by workers to check if their request has been resolved
|
||||
*
|
||||
@@ -352,6 +365,10 @@ export async function readResolvedPermission(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use sendPermissionResponseViaMailbox() instead. This file-based
|
||||
* approach writes to an unauthenticated directory where any local process can
|
||||
* forge approvals. Retained for backward compatibility but no longer called.
|
||||
*
|
||||
* Resolve a permission request
|
||||
* Called by the team leader (or worker in self-resolution cases)
|
||||
*
|
||||
@@ -536,6 +553,10 @@ export type PermissionResponse = {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use processMailboxPermissionResponse() via useInboxPoller instead.
|
||||
* File-based polling reads from an unauthenticated directory where any local
|
||||
* process can forge approval files. Retained for backward compatibility.
|
||||
*
|
||||
* Poll for a permission response (worker-side convenience function)
|
||||
* Converts the resolved request into a simpler response format
|
||||
*
|
||||
@@ -564,6 +585,9 @@ export async function pollForResponse(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated File-based response cleanup is no longer needed — responses are
|
||||
* delivered via mailbox. Retained for backward compatibility.
|
||||
*
|
||||
* Remove a worker's response after processing
|
||||
* This is an alias for deleteResolvedPermission for backward compatibility
|
||||
*/
|
||||
@@ -601,6 +625,9 @@ export function isSwarmWorker(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated File-based resolved permissions are no longer written. Responses
|
||||
* are delivered via mailbox. Retained for backward compatibility.
|
||||
*
|
||||
* Delete a resolved permission file
|
||||
* Called after a worker has processed the resolution
|
||||
*/
|
||||
@@ -635,8 +662,8 @@ export async function deleteResolvedPermission(
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a permission request (alias for writePermissionRequest)
|
||||
* Provided for backward compatibility with worker integration code
|
||||
* @deprecated Alias for writePermissionRequest, which is itself deprecated.
|
||||
* Use sendPermissionRequestViaMailbox() instead.
|
||||
*/
|
||||
export const submitPermissionRequest = writePermissionRequest
|
||||
|
||||
|
||||
Reference in New Issue
Block a user