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:
- Run
cargo build from the repo root.
- Copy the script below to
poc/force-remove-local-global-mismatch.
- 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
- 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.
- 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.
- Bug vs by-design: BUG — bypassing the local-storage safeguard changes the deletion target, not just the prompt behavior promised by
--force.
- Final severity: Medium — this is an unintended deletion of a different same-user config file in another config scope.
- In scope: YES — the behavior is concrete, reproducible, and does not rely on privileged machine access.
- Test correctness: CORRECT — it checks the real filesystem side effects on both candidate targets and does not rely on tautological assertions.
- Alternative explanations: NONE
- 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.
007:
keys rm --forceDeletes the Global Alias After Resolving the Local OneDate: 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> --forcecan 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 rmonly enforces the local-storage safety check on the non---forcepath. Under--force,locator::Args::remove_identityreads the identity throughread_identity(which searches local before global) and then callsKeyType::Identity.remove(name, &self.config_dir()?), whereconfig_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.tomland a different globalidentity/alice.tomlat the same time. Runningstellar keys rm alice --forcefrom the project directory resolvesalicefrom the local config but deletes the global file instead.Affected Code
cmd/soroban-cli/src/commands/keys/rm.rs:37-65—--forceskips the local-storage guard and goes straight toremove_identitycmd/soroban-cli/src/config/locator.rs:157-166— local/global lookup order is local first, butconfig_dir()returns the global config directorycmd/soroban-cli/src/config/locator.rs:322-332— removal reads one identity and deletes by a different directory sourcecmd/soroban-cli/src/config/locator.rs:647-664— local identities win duringread_with_global_with_locationcmd/soroban-cli/src/config/locator.rs:782-790— file deletion happens at the path built from the global config directoryPoC
poc/force-remove-local-global-mismatchcargo buildfrom the repo root.poc/force-remove-local-global-mismatch.bash poc/force-remove-local-global-mismatchTest Body
Expected vs Actual Behavior
keys rm --forceshould remove the same resolved identity, or reject deprecated local identities even when--forceis used.Adversarial Review
keys public-keyand then showskeys rm --forcedeletes only the global file.keys ls --long, and--forceis documented only as skipping confirmation.--force.Suggested Fix
Preserve the resolved location all the way through removal.
keys rmshould either keep rejectingLocation::Localidentities even with--force, or pass the resolved location fromread_identity_with_locationinto deletion so the same alias file that was read is the one that gets removed.