Skip to content

keys rm --force Deletes the Global Alias After Resolving the Local One #2495

@fnando

Description

@fnando

007: keys rm --force Deletes the Global Alias After Resolving the Local One

Date: 2026-04-17
Severity: Medium
Impact: unintended file write outside expected paths
Subsystem: keys
Final review by: gpt-5.4, high

Summary

stellar keys rm <name> --force can remove the wrong identity file when the same alias exists in both deprecated local config and the global profile. Resolution is local-first, but deletion is hard-wired to the global config directory, so the local alias remains while the different global alias is deleted.

Root Cause

keys rm only enforces the local-storage safety check on the non---force path. Under --force, locator::Args::remove_identity reads the identity through read_identity (which searches local before global) and then calls KeyType::Identity.remove(name, &self.config_dir()?), where config_dir() always resolves to the global or explicitly selected config directory.

Reproduction

This manifests when a user has a deprecated project-local .stellar/identity/alice.toml and a different global identity/alice.toml at the same time. Running stellar keys rm alice --force from the project directory resolves alice from the local config but deletes the global file instead.

Affected Code

  • cmd/soroban-cli/src/commands/keys/rm.rs:37-65--force skips the local-storage guard and goes straight to remove_identity
  • cmd/soroban-cli/src/config/locator.rs:157-166 — local/global lookup order is local first, but config_dir() returns the global config directory
  • cmd/soroban-cli/src/config/locator.rs:322-332 — removal reads one identity and deletes by a different directory source
  • cmd/soroban-cli/src/config/locator.rs:647-664 — local identities win during read_with_global_with_location
  • cmd/soroban-cli/src/config/locator.rs:782-790 — file deletion happens at the path built from the global config directory

PoC

  • Target test file: poc/force-remove-local-global-mismatch
  • Test name: force-remove-local-global-mismatch
  • Test language: bash
  • How to run:
  1. Run cargo build from the repo root.
  2. Copy the script below to poc/force-remove-local-global-mismatch.
  3. Run: bash poc/force-remove-local-global-mismatch

Test Body

#!/usr/bin/env bash
set -euo pipefail

STELLAR="$(cd /repo/stellar-cli && pwd)/target/debug/stellar"

WORKDIR=$(mktemp -d)
GLOBALDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR" "$GLOBALDIR"' EXIT

LOCAL_ID_DIR="$WORKDIR/.stellar/identity"
GLOBAL_ID_DIR="$GLOBALDIR/identity"
mkdir -p "$LOCAL_ID_DIR" "$GLOBAL_ID_DIR"

cat > "$LOCAL_ID_DIR/alice.toml" <<'EOF'
seed_phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
EOF

cat > "$GLOBAL_ID_DIR/alice.toml" <<'EOF'
seed_phrase = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"
EOF

echo "=== Before rm --force ==="
echo "LOCAL  exists: $(test -f "$LOCAL_ID_DIR/alice.toml" && echo YES || echo NO)"
echo "GLOBAL exists: $(test -f "$GLOBAL_ID_DIR/alice.toml" && echo YES || echo NO)"

RESOLVED_PUBKEY=$(cd "$WORKDIR" && STELLAR_CONFIG_HOME="$GLOBALDIR" "$STELLAR" keys public-key alice 2>/dev/null || true)
echo "Resolved public key (should be from LOCAL seed): $RESOLVED_PUBKEY"

cd "$WORKDIR"
STELLAR_CONFIG_HOME="$GLOBALDIR" "$STELLAR" keys rm alice --force 2>/dev/null || true

echo
echo "=== After rm --force ==="
LOCAL_EXISTS=$(test -f "$LOCAL_ID_DIR/alice.toml" && echo YES || echo NO)
GLOBAL_EXISTS=$(test -f "$GLOBAL_ID_DIR/alice.toml" && echo YES || echo NO)
echo "LOCAL  exists: $LOCAL_EXISTS"
echo "GLOBAL exists: $GLOBAL_EXISTS"

PASS=true

if [ "$LOCAL_EXISTS" = "YES" ]; then
    echo "PASS: Local alice.toml was not deleted"
else
    echo "FAIL: Local alice.toml was unexpectedly deleted"
    PASS=false
fi

if [ "$GLOBAL_EXISTS" = "NO" ]; then
    echo "PASS: Global alice.toml was deleted instead"
else
    echo "FAIL: Global alice.toml still exists"
    PASS=false
fi

if [ "$PASS" = true ]; then
    echo "=== PoC CONFIRMED ==="
    exit 0
else
    echo "=== PoC NOT CONFIRMED ==="
    exit 1
fi

Expected vs Actual Behavior

  • Expected: keys rm --force should remove the same resolved identity, or reject deprecated local identities even when --force is used.
  • Actual: the command resolves the local alias, leaves that local file in place, and deletes the different global alias file.

Adversarial Review

  1. Exercises claimed bug: YES — the PoC proves local-first resolution with keys public-key and then shows keys rm --force deletes only the global file.
  2. Realistic preconditions: YES — the CLI still supports deprecated local config, duplicate local/global aliases are visible via keys ls --long, and --force is documented only as skipping confirmation.
  3. Bug vs by-design: BUG — bypassing the local-storage safeguard changes the deletion target, not just the prompt behavior promised by --force.
  4. Final severity: Medium — this is an unintended deletion of a different same-user config file in another config scope.
  5. In scope: YES — the behavior is concrete, reproducible, and does not rely on privileged machine access.
  6. Test correctness: CORRECT — it checks the real filesystem side effects on both candidate targets and does not rely on tautological assertions.
  7. Alternative explanations: NONE
  8. Novelty: NOVEL

Suggested Fix

Preserve the resolved location all the way through removal. keys rm should either keep rejecting Location::Local identities even with --force, or pass the resolved location from read_identity_with_location into deletion so the same alias file that was read is the one that gets removed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Backlog (Not Ready)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions