Claude-Agent

 view release on metacpan or  search on metacpan

lib/Claude/Agent/MCP/SDKRunner.pm  view on Meta::CPAN

        push @allowed_prefixes, "$ENV{HOME}/tmp/";
        push @allowed_prefixes, "$ENV{HOME}/.tmp/";
    }
    # Allow TMPDIR if set (File::Temp respects this)
    # *** SECURITY WARNING ***
    # TMPDIR is user-controllable and NOT validated for trust.
    # KNOWN RISK: An attacker who can set TMPDIR before process startup
    # could influence which socket paths are allowed, potentially enabling
    # connections to attacker-controlled sockets.
    #
    # FOR HIGH-SECURITY DEPLOYMENTS: Set CLAUDE_AGENT_IGNORE_TMPDIR=1
    #
    # Additional mitigations:
    #   1. Set CLAUDE_AGENT_IGNORE_TMPDIR=1 to ignore TMPDIR entirely (RECOMMENDED)
    #   2. Validate socket ownership with stat() before connecting
    #   3. Use only fixed prefixes by not setting TMPDIR
    #   4. Run in a restricted environment where TMPDIR cannot be manipulated
    #   5. Set TMPDIR to a trusted directory (e.g., /tmp) before process startup
    # Only allow TMPDIR when explicitly enabled via CLAUDE_AGENT_ALLOW_TMPDIR=1
    # This is opt-in for stricter security - TMPDIR could be attacker-controlled
    # SECURITY WARNING: NEVER set CLAUDE_AGENT_ALLOW_TMPDIR=1 in untrusted environments
    # or when an attacker could control environment variables before process startup.
    # An attacker could set both CLAUDE_AGENT_ALLOW_TMPDIR=1 and a malicious TMPDIR
    # to redirect socket connections to attacker-controlled locations.
    if ($ENV{TMPDIR} && $ENV{TMPDIR} =~ m{^/} && $ENV{CLAUDE_AGENT_ALLOW_TMPDIR}) {
        # SECURITY: Log warning when TMPDIR override is used
        warn "[SECURITY WARNING] TMPDIR-based socket path allowed via CLAUDE_AGENT_ALLOW_TMPDIR. "
            . "This is insecure if an attacker can control environment variables.\n"
            unless $ENV{CLAUDE_AGENT_QUIET_SECURITY_WARNINGS};
        push @allowed_prefixes, $ENV{TMPDIR};
        push @allowed_prefixes, "$ENV{TMPDIR}/" unless $ENV{TMPDIR} =~ m{/$};
    }

    require Cwd;
    my $resolved_path = Cwd::abs_path($socket_path);
    die "Invalid socket path: cannot resolve\n" unless defined $resolved_path;
    for my $prefix (@allowed_prefixes) {
        my $resolved_prefix = Cwd::abs_path($prefix);
        next unless defined $resolved_prefix;
        if (index($resolved_path, $resolved_prefix) == 0) {
            $valid_socket_path = 1;
            last;
        }
    }
    die "Invalid socket path: must be within a temporary directory (/tmp, /var/tmp, TMPDIR, etc.)\n"
        unless $valid_socket_path;

    # Validate server_name - alphanumeric with hyphens/underscores only
    die "Invalid server name: must be alphanumeric with hyphens/underscores\n"
        unless $server_name =~ /^[a-zA-Z0-9_-]{1,100}$/;

    # Validate version if provided - semver-like format
    die "Invalid version format\n"
        if defined($version) && length($version) && $version !~ /^[a-zA-Z0-9._-]{1,50}$/;

    # Limit tools_json size to prevent memory exhaustion (1MB limit)
    die "tools_json too large (max 1MB)\n" if length($tools_json) > 1_000_000;

    my ($tools) = $state->{jsonl}->decode($tools_json);

    # Validate decoded structure: must be an array of tool definitions
    die "Invalid tools_json: expected array\n" unless ref $tools eq 'ARRAY';
    for my $tool (@$tools) {
        die "Invalid tool definition: expected hash with 'name' key\n"
            unless ref $tool eq 'HASH' && defined $tool->{name};
    }

    # Build tool lookup
    my %tool_by_name = map { $_->{name} => $_ } @$tools;

    # Validate socket ownership before connecting (defense-in-depth)
    # This helps detect if an attacker has replaced the socket with one they control
    {
        my @stat_info = stat($socket_path);
        if (@stat_info) {
            my $socket_uid = $stat_info[4];
            if ($socket_uid != $<) {
                die "Security error: socket '$socket_path' is owned by uid $socket_uid, expected uid $< (current user)\n";
            }
        }
        # If stat fails, the socket may not exist yet - let the connect() call handle it
    }

    # Connect to parent socket
    $state->{socket} = IO::Socket::UNIX->new(
        Type => SOCK_STREAM,
        Peer => $socket_path,
    ) or die "Cannot connect to socket $socket_path: $!\n";

    $state->{socket}->autoflush(1);

    $log->debug(sprintf("SDKRunner: Initializing with socket: %s", $socket_path));
    $log->debug("SDKRunner: Connected to parent socket");

    # Create IO::Async event loop
    $state->{loop} = IO::Async::Loop->new;

    # Track running state
    my $running = 1;

    # Shutdown helper
    my $shutdown = sub {
        return unless $running;
        $running = 0;
        $state->{loop}->stop;
    };

    # Handle signals for graceful shutdown
    local $SIG{TERM} = $shutdown;
    local $SIG{PIPE} = $shutdown;

    # Create async stream for STDIN (from Claude CLI)
    my $stdin_stream = IO::Async::Stream->new(
        read_handle => \*STDIN,
        on_read => sub {
            my ($stream, $buffref) = @_;

            while ($$buffref =~ s/^([^\n]+)\n//) {
                my $line = $1;
                next unless length $line;



( run in 1.098 second using v1.01-cache-2.11-cpan-9581c071862 )