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
- 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
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