HarperFast / HarperFast/harper-pro

add_ssh_key: TOCTOU between the duplicate-name check and the key write hands one caller an unusable generated deploy key

Open
#693 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
JavaScript
Stars
3
Forks
0
Avg merge
1d 21h
Merged PRs (30d)
80

Description

`add_ssh_key` checks whether a key name is taken and then writes it as two separate steps, with `await`s in between. Concurrent adds for the same name can both pass the check and both write.

https://github.com/HarperFast/harper-pro/blob/main/security/sshKeyOperations.ts#L170 rejects a duplicate:

```ts
const { filePath, configFile, knownHostsFile } = getSSHPaths(req.name);
if (await exists(filePath)) {
throw new ClientError('Key already exists. ...');
}
```

and the write lands ~26 lines later, at L196, after an `await` on keypair generation (L180) and the sealing step (L192):

```ts
await writeFileEnsureDir(filePath, storedKey, 0o600);
```

`writeFileEnsureDir` calls `writeFile` with no `flag`, so it defaults to `'w'` — create-or-truncate. Nothing between the check and the write reserves the name.

## Why it matters more for `generate: true`

With a caller-supplied `key`, two racing adds converge on the same material, so a lost write is mostly harmless. With `generate: true` each request mints a *different* keypair, so the outcome is a key that cannot authenticate:

1. Request A and request B both call `add_ssh_key name=deploy generate=true`.
2. Both observe no `deploy.key` and proceed.
3. A mints pair A and writes it; B mints pair B and overwrites with pair B.
4. Both return `200` — A with `public_key` A, B with `public_key` B.
5. Only private key B survives on disk.

Whoever registered public key A with their git host now has a deploy key that will never authenticate, and nothing in the response indicated a problem. A client retry after a timeout is enough to trigger this, since the first request may still be in flight.

The SSH config block has the same shape of problem: L209 appends unconditionally, so a race leaves two `#deploy` blocks in `ssh/config` for one key.

## Suggested fix

Per @kriszyp's review on #594: atomically reserve the name before generating — `writeFile` with the `wx` flag, or a per-name lock — and return the existing duplicate-name `ClientError` to whichever request loses. The config-file update should be inside the same serialized section so the block and the key file cannot disagree.

Worth confirming the intended behavior for the replicated path too: peers re-run the op with `key` already present, so a reservation scheme needs to leave replication able to overwrite deliberately where that is correct.

## Provenance

Raised by @kriszyp reviewing https://github.com/HarperFast/harper-pro/pull/594 (approved; this was flagged as follow-up, not a blocker). Line references are against `main` after that PR merged its rebase.

Contributor guide

Open the contributing guide

Research direction

Start in security/sshKeyOperations.ts around the duplicate check at line 170, key generation and sealing, and the write/config update near lines 196-209. Trace concurrent generate=true calls and the replicated path to establish the intended reservation and overwrite behavior. Done means losing callers receive the existing duplicate-name error, key and config updates are serialized, and replication remains correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.