Skip to content

cli: 'identity import' extracts secret keys with archive-controlled permissions and does not reject symlink members #137

Description

@Dione-b

Summary

ctg identity import (packages/cli/src/commands/identity.command.ts:87-104) unpacks an archive containing Stellar secret keys into ~/.config/stellar. The recent path-traversal hardening (464221b) closed the ../ case, but two gaps remain in untarDirectory (identity.command.ts:36-62).

1. Archive-controlled file permissions

await mkdir(targetDir, { recursive: true, mode: 0o700 });
await assertNoPathTraversal(archiveFile, targetDir);
await execa("tar", ["-xzf", archiveFile, "-C", targetDir], { stdio: "inherit" });

The 0o700 applies to the target directory only. GNU tar restores each member's recorded mode, so an archive whose entries are stored 0644/0666 yields world-readable secret keys inside a 0700 directory — and if any intermediate directory in the archive is stored 0755, the containing directory becomes traversable too. On a shared CI runner or multi-user host, that exposes signing keys to any local user.

The rest of the command is careful about exactly this (withSecureTempDir uses mkdtemp + rm, the staged archive is written with mode: 0o600) — the extraction step is the one place the guarantee is dropped.

2. Symlink escape is not covered by a name-only pre-check

assertNoPathTraversal runs tar -tzf and resolves each entry name against the target. It cannot see entry types. An archive containing:

link            -> symlink to /home/victim/.ssh
link/authorized_keys

passes the check — path.resolve(target, "link/authorized_keys") stays inside target — but extraction may write through the symlink created moments earlier by the same archive. This is the classic node-tar/Zip-Slip-adjacent symlink escape; GNU tar's defenses against it vary by version and flags, so relying on them implicitly is fragile.

There is also a TOCTOU gap in principle (list pass and extract pass are two separate reads of the file), though it is small here since the archive is staged in a 0700 temp dir the command itself owns.

Failure scenario

A developer follows a CI runbook and runs ctg identity import ci-identity.b64 on a shared build machine, using an archive produced by an untrusted or compromised pipeline. The archive's members are stored mode 0644; every other user on that host can now read the deployer's Stellar secret key. A second archive with a symlink member writes into ~/.ssh/authorized_keys instead of the Stellar config dir.

Suggested fix

  • Extract with --no-same-permissions --no-same-owner and a restrictive umask (or chmod -R go-rwx the target immediately after extraction) so imported key material is never group/world readable.
  • Reject archives containing symlink or hardlink members outright. tar -tvzf (verbose) exposes the type character, or switch to a library extractor with an explicit filter that rejects non-regular-file/non-directory entries.
  • Consider --no-overwrite-dir and extracting to a fresh staging dir, then moving into place, so a partially-validated archive never touches ~/.config/stellar.

Scope: audit was read-only, no code changed. Related to the already-shipped traversal fix in 464221b — this is the remainder of that surface.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Stellar WaveIssues in the Stellar wave programarea: file-ioAtomic writes, file operationspriority: highHigh priority fix - data corruption, security, or recovery failure risk

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions