openpencil/vendor/anthropic-agent-sdk/SECURITY.md

10 KiB

Security Documentation

This document describes the security measures implemented in the Claude Agent SDK for Rust.

Overview

The SDK implements multiple layers of defense to ensure secure operation when interfacing with the Claude Code CLI.

Security Measures

1. Environment Variable Blocking (Strict Enforcement)

Dangerous environment variables that could be exploited for code injection cause an immediate error - they are not silently filtered:

const DANGEROUS_ENV_VARS: &[&str] = &[
    "LD_PRELOAD",           // Linux dynamic linker injection
    "LD_LIBRARY_PATH",      // Linux library path manipulation
    "DYLD_INSERT_LIBRARIES", // macOS dylib injection
    "DYLD_LIBRARY_PATH",    // macOS library path manipulation
    "PATH",                 // Command search path manipulation
    "NODE_OPTIONS",         // Node.js runtime injection
    "PYTHONPATH",           // Python module injection
    "PERL5LIB",             // Perl module injection
    "RUBYLIB",              // Ruby library injection
];

Behavior:

  • Safe env vars: Passed to subprocess normally
  • Dangerous env vars: Returns ClaudeError::InvalidConfig with details
  • Logs warning (with tracing-support feature)
// This will fail with an error
options.env.insert("LD_PRELOAD".to_string(), "/tmp/evil.so".to_string());
let client = ClaudeSDKClient::new(options, None).await;
// Error: "Dangerous environment variables detected: [LD_PRELOAD]. These are blocked to prevent injection attacks."

Location: src/transport/subprocess.rs:23-33, enforced at :549-572

2. CLI Argument Allowlist (Strict Enforcement)

Only explicitly allowed CLI flags can be passed via extra_args. Disallowed flags cause an immediate error - they are not silently ignored:

const ALLOWED_EXTRA_FLAGS: &[&str] = &["timeout", "retries", "log-level", "cache-dir"];

Behavior:

  • Allowed flags: Passed to CLI normally
  • Disallowed flags: Returns ClaudeError::InvalidConfig with details
  • Logs warning (with tracing-support feature)
// This will fail with an error
options.extra_args.insert("dangerous-flag".to_string(), None);
let client = ClaudeSDKClient::new(options, None).await;
// Error: "Disallowed CLI flags in extra_args: [dangerous-flag]. Allowed flags: [...]"

Location: src/transport/subprocess.rs:36, enforced at :426-461

Validation Order and Precedence

Both checks run during connect(). CLI args are validated first, then env vars.

CLI args use an allowlist (whitelist):

  • Only flags explicitly listed in ALLOWED_EXTRA_FLAGS are permitted
  • Everything else is rejected

Env vars use a blocklist (blacklist):

  • Only vars listed in DANGEROUS_ENV_VARS are rejected
  • Everything else is permitted

There is no way to override these at runtime. The lists are compile-time constants. Attempting to pass a blocked item always fails - there is no "force" option by design.

3. Buffer Size Limits

Memory exhaustion attacks are prevented by limiting JSON message sizes:

  • Default limit: 1MB (DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024)
  • Configurable: Via ClaudeAgentOptions::max_buffer_size
  • Messages exceeding the limit generate an error and are discarded

Location: src/transport/subprocess.rs:20, enforced at subprocess.rs:688-699

4. Timeout Strategy (CancellationToken-based)

The SDK uses cooperative cancellation instead of hardcoded timeouts for I/O operations.

Design Rationale

Hardcoded timeouts (e.g., 30-second read timeouts) were considered but rejected because:

  1. Long-running operations are legitimate: Claude Code queries involving subagent research, complex code analysis, or multi-step agentic workflows can take minutes
  2. False positive risk: A timeout would interrupt valid operations, corrupting state
  3. User control is preferable: Applications know their expected operation durations better than the SDK

Implementation

The SDK implements CancellationToken (analogous to JavaScript's AbortController):

// Client provides cancellation control
let token = client.cancellation_token();

// Application can cancel at any time
client.cancel();

// Background tasks respect cancellation
tokio::select! {
    _ = cancel_token.cancelled() => { /* cleanup */ }
    result = read_operation => { /* process */ }
}

Locations:

  • ClaudeSDKClient::cancellation_token() - Get a child token
  • ClaudeSDKClient::cancel() - Cancel all operations
  • SubprocessTransport::read_messages() - Respects cancellation in read loop

Graceful Shutdown

The close() method implements a graceful shutdown with a 5-second timeout before forcing process termination:

// Try graceful exit first
let timeout_duration = Duration::from_secs(5);
match tokio::time::timeout(timeout_duration, child.wait()).await {
    Ok(_) => { /* graceful exit */ }
    Err(_) => {
        child.kill().await; // Force kill after timeout
    }
}

Location: src/transport/subprocess.rs:792-807

5. No Unsafe Code

The SDK is 100% safe Rust with no unsafe blocks. This ensures memory safety guarantees are enforced by the compiler.

6. Input Bounds Checking

Configurable values are bounded to prevent abuse:

  • max_turns: Maximum 1000 turns (ClaudeAgentOptions::MAX_ALLOWED_TURNS)

7. Secure Debug Output

Sensitive data (callbacks, hooks) are redacted in Debug implementations:

impl Debug for ClaudeAgentOptions {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("ClaudeAgentOptions")
            .field("can_use_tool", &self.can_use_tool.as_ref().map(|_| "<callback>"))
            // ...
    }
}

Location: src/types/options.rs:442-514

8. Stderr Isolation

The subprocess stderr is piped (not inherited) to prevent the child process from manipulating the parent terminal state:

cmd.stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped()); // Prevents terminal manipulation

Location: src/transport/subprocess.rs:538-540

9. Session Binding (Secure by Default)

The SDK automatically binds to a session on the first Result message, preventing messages from being sent to unintended conversation contexts.

Design Rationale

Without session binding, a multi-turn application could accidentally send messages to a different session if:

  • Session IDs get mixed up in application state
  • Race conditions in concurrent applications
  • Unexpected session restarts

Implementation

Session binding is automatic and secure by default:

// Auto-binds on first Result - no manual binding required
client.send_message("Hello").await?;
while let Some(msg) = client.next_message().await {
    if let Message::Result { .. } = msg? {
        // Session is now auto-bound!
        break;
    }
}

// All subsequent sends validate automatically
client.send_message("Follow-up").await?;  // Validates session first

If the current session doesn't match the bound session, send_message() returns ClaudeError::SessionMismatch:

#[error("Session mismatch: expected {expected}, got {actual}")]
SessionMismatch {
    expected: String,
    actual: String,
}

Override Methods

For advanced use cases (e.g., multi-session applications):

Method Description
bind_session(id) Override auto-bound session
unbind_session() Disable validation (use with caution)
validate_session() Manual validation check
bound_session() Get currently bound session

Subagent Behavior

Subagents (via Task tool) share the parent session ID, so auto-bind works correctly:

  • Parent binds to session on first Result
  • Subagent hooks report same session ID
  • No SessionMismatch conflicts

Locations:

  • Auto-bind: src/client.rs:449-454 (in message_reader_task)
  • Validation: src/client.rs (validate_session(), send_message())
  • Error type: src/error.rs:100-107

10. OAuth Token Security

The OAuth module stores authentication tokens with appropriate security measures:

Token Storage

  • Location: Platform-specific config directory
    • macOS: ~/Library/Application Support/claude-sdk/oauth_token.json
    • Linux: ~/.config/claude-sdk/oauth_token.json
    • Windows: %APPDATA%\claude-sdk\oauth_token.json
  • Permissions: 0600 on Unix (owner read/write only)
  • Contents: Access token, refresh token, expiration timestamp
#[cfg(unix)]
{
    use std::os::unix::fs::PermissionsExt;
    let perms = std::fs::Permissions::from_mode(0o600);
    std::fs::set_permissions(&storage_path, perms)?;
}

Token Handling

  • Expiration buffer: Tokens considered expired 60 seconds before actual expiry
  • Auto-refresh: Expired tokens trigger automatic refresh via refresh_token
  • PKCE: Authorization uses PKCE (SHA-256 challenge) to prevent code interception
  • No secrets in logs: Token values are truncated in display output

Logout

The logout() method deletes the cached token file entirely.

Location: src/auth/token.rs:172-213, src/auth/oauth.rs

Threat Model

Threat Mitigation
CLI command injection Argument allowlist, environment filtering
Memory exhaustion Buffer size limits
Infinite hangs CancellationToken, graceful shutdown timeout
Terminal corruption Stderr isolation
Memory unsafety 100% safe Rust
Session confusion/hijacking Auto-bind on first Result, validation on send
OAuth token theft Restrictive file permissions (0600), PKCE for auth
OAuth code interception PKCE with SHA-256 challenge/verifier

Reporting Security Issues

Please report security vulnerabilities to the maintainers via GitHub security advisories.