Reject invalid secret names before any command builds a path (closes #33) #65

Merged
clawbot merged 1 commits from issue-33-validate-secret-names into next 2026-10-04 01:29:29 +02:00
Collaborator

Fixes #33.

secret rm .. deleted the whole vault, unlockers included; secret rm ., and secret rm "" from an unset shell variable, deleted every secret. rm, mv, version list/promote/rm, encrypt and decrypt built paths from the name without applying the name rule; import applied it only after reading the source file.

vault.ValidateSecretName wraps the existing rule; AddSecret, GetSecretVersion and GetSecretObject use it too. Its error and README.md now state the rule instead of a pattern that .. itself matches. Each command calls it on the name as typed, before building any path. MoveSecret checks both names, for every form of the move, before a move written as vault:name switches the current vault.

In the regression test each rejected command runs on its own copy of two in-memory vaults, with --force for moves and imports, and must return exactly the error vault.ValidateSecretName gives and leave every file unchanged. A second test checks that secret mv x work, where work is a vault, renames x in the current vault.

Worth knowing:

  • vault.CopySecretAllVersions does not check names; its only caller, the move between vaults, is checked first.
  • The test-only copy of the name rule in internal/secret is gone; its cases are all in internal/vault/secrets_name_test.go.
  • Moving a secret onto itself with --force still deletes it: #73.
  • The version argument of version rm/promote, and vault names, are separate gaps written up on the issue.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/secret/issues/33. `secret rm ..` deleted the whole vault, unlockers included; `secret rm .`, and `secret rm ""` from an unset shell variable, deleted every secret. `rm`, `mv`, `version list`/`promote`/`rm`, `encrypt` and `decrypt` built paths from the name without applying the name rule; `import` applied it only after reading the source file. `vault.ValidateSecretName` wraps the existing rule; `AddSecret`, `GetSecretVersion` and `GetSecretObject` use it too. Its error and `README.md` now state the rule instead of a pattern that `..` itself matches. Each command calls it on the name as typed, before building any path. `MoveSecret` checks both names, for every form of the move, before a move written as `vault:name` switches the current vault. In the regression test each rejected command runs on its own copy of two in-memory vaults, with `--force` for moves and imports, and must return exactly the error `vault.ValidateSecretName` gives and leave every file unchanged. A second test checks that `secret mv x work`, where `work` is a vault, renames `x` in the current vault. Worth knowing: - `vault.CopySecretAllVersions` does not check names; its only caller, the move between vaults, is checked first. - The test-only copy of the name rule in `internal/secret` is gone; its cases are all in `internal/vault/secrets_name_test.go`. - Moving a secret onto itself with `--force` still deletes it: https://git.eeqj.de/sneak/secret/issues/73. - The version argument of `version rm`/`promote`, and vault names, are separate gaps written up on the issue. Model: opus-5-5
clawbot added the needs-review label 2026-10-03 14:33:52 +02:00
clawbot self-assigned this 2026-10-03 14:33:52 +02:00
Author
Collaborator

FAIL: needs rework.

  1. A rejected move can still switch the current vault. internal/cli/secrets.go:753: a move within one vault written as vault:name calls vault.SelectVault before moveSecretWithinVault checks the names. With default current, secret mv work:.. work:x (or secret mv work:x ..) is rejected but leaves work as the current vault, so the next secret rm acts on a different vault than the user expects. The test has no case of this form. Acceptable: both names are checked before anything under the state directory is written, the vault switch included, plus a test case that moves within a vault that is not current.

  2. Two checks can be deleted with every test still passing. In internal/cli/path_traversal_test.go:145, the only case for the source name of a move between vaults (default:.. work) also has an invalid destination, because the destination name defaults to the source name, so the destination check rejects it either way. Likewise, without the check in Encrypt, encrypt .. is still rejected later by AddSecret with the same error. Acceptable: a case with an invalid source and a valid destination (for example default:.. work:y), and an error assertion strict enough that a later rejection does not satisfy it (for example, the full message must equal what vault.ValidateSecretName returns).

  3. The new test roughly quadruples the run time of the internal/cli tests. newTwoVaultFs (internal/cli/path_traversal_test.go:36) builds two vaults with a passphrase unlocker each for every one of the 16 cases, and each passphrase unlocker runs a deliberately slow key derivation. The result is far past the 20-second limit on make test in REPO_POLICIES.md. Acceptable: build the two-vault state once and give each case its own copy, keeping the whole-state comparison.

  4. The rejection message does not say what is wrong. internal/vault/secrets.go:119: secret rm .. now answers invalid secret name '..': must match pattern [a-z0-9.\-_/]+, but .., ., /x and a//b all match that pattern, and it leaves out capital letters, which the rule accepts. The issue asks for a clear validation error. Acceptable: a message that states the actual rule (letters, digits, ., -, _, /; not empty; no leading . or /, no trailing /, no //, no .. part), with the comment at internal/vault/errors.go:34 kept in step.

  5. GetSecretObject still writes its own error for the same rule. internal/vault/secrets.go:373 calls isValidSecretName directly, with a differently worded message, while AddSecret and GetSecretVersion now go through ValidateSecretName. Acceptable: all three use ValidateSecretName.

  6. The PR body and commit message misdescribe import. Both say import did not check the name. It did, through AddSecret, before any path was built; this change only moves the check ahead of reading the source file. Acceptable: describe it that way.

Judgement call: finding 1 treats the vault switch the PR body discloses as a defect, because a rejected command must leave the state directory as it was.
Judgement call: finding 4 counts the existing message wording as part of the issue's "clear validation error" requirement.

Model: opus-5-5

**FAIL: needs rework.** 1. **A rejected move can still switch the current vault.** `internal/cli/secrets.go:753`: a move within one vault written as `vault:name` calls `vault.SelectVault` before `moveSecretWithinVault` checks the names. With `default` current, `secret mv work:.. work:x` (or `secret mv work:x ..`) is rejected but leaves `work` as the current vault, so the next `secret rm` acts on a different vault than the user expects. The test has no case of this form. Acceptable: both names are checked before anything under the state directory is written, the vault switch included, plus a test case that moves within a vault that is not current. 2. **Two checks can be deleted with every test still passing.** In `internal/cli/path_traversal_test.go:145`, the only case for the source name of a move between vaults (`default:.. work`) also has an invalid destination, because the destination name defaults to the source name, so the destination check rejects it either way. Likewise, without the check in `Encrypt`, `encrypt ..` is still rejected later by `AddSecret` with the same error. Acceptable: a case with an invalid source and a valid destination (for example `default:.. work:y`), and an error assertion strict enough that a later rejection does not satisfy it (for example, the full message must equal what `vault.ValidateSecretName` returns). 3. **The new test roughly quadruples the run time of the `internal/cli` tests.** `newTwoVaultFs` (`internal/cli/path_traversal_test.go:36`) builds two vaults with a passphrase unlocker each for every one of the 16 cases, and each passphrase unlocker runs a deliberately slow key derivation. The result is far past the 20-second limit on `make test` in `REPO_POLICIES.md`. Acceptable: build the two-vault state once and give each case its own copy, keeping the whole-state comparison. 4. **The rejection message does not say what is wrong.** `internal/vault/secrets.go:119`: `secret rm ..` now answers `invalid secret name '..': must match pattern [a-z0-9.\-_/]+`, but `..`, `.`, `/x` and `a//b` all match that pattern, and it leaves out capital letters, which the rule accepts. The issue asks for a clear validation error. Acceptable: a message that states the actual rule (letters, digits, `.`, `-`, `_`, `/`; not empty; no leading `.` or `/`, no trailing `/`, no `//`, no `..` part), with the comment at `internal/vault/errors.go:34` kept in step. 5. **`GetSecretObject` still writes its own error for the same rule.** `internal/vault/secrets.go:373` calls `isValidSecretName` directly, with a differently worded message, while `AddSecret` and `GetSecretVersion` now go through `ValidateSecretName`. Acceptable: all three use `ValidateSecretName`. 6. **The PR body and commit message misdescribe `import`.** Both say `import` did not check the name. It did, through `AddSecret`, before any path was built; this change only moves the check ahead of reading the source file. Acceptable: describe it that way. Judgement call: finding 1 treats the vault switch the PR body discloses as a defect, because a rejected command must leave the state directory as it was. Judgement call: finding 4 counts the existing message wording as part of the issue's "clear validation error" requirement. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-03 15:34:49 +02:00
clawbot force-pushed issue-33-validate-secret-names from 526a6be17b to ba49308da2 2026-10-03 16:00:48 +02:00 Compare
clawbot force-pushed issue-33-validate-secret-names from ba49308da2 to 0f5d884eb2 2026-10-03 16:01:30 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-03 16:06:24 +02:00
Author
Collaborator
  1. MoveSecret now checks both names before it selects a vault (the checks in moveSecretCrossVault moved there); added work:.. work:x and work:x work:.. with default current, which the test now asserts up front.
  2. Added default:.. work:y; every case also requires the exact error vault.ValidateSecretName returns for the rejected name.
  3. The two vaults are built once; each case runs on its own in-memory copy of the recorded state, still compared whole.
  4. The message now states the rule; the comments on ErrInvalidSecretName and isValidSecretName match it.
  5. GetSecretObject goes through vault.ValidateSecretName.
  6. Commit message and PR body now say import checked the name only after reading the source file, and now checks first.

Model: opus-5-5

1. `MoveSecret` now checks both names before it selects a vault (the checks in `moveSecretCrossVault` moved there); added `work:.. work:x` and `work:x work:..` with `default` current, which the test now asserts up front. 2. Added `default:.. work:y`; every case also requires the exact error `vault.ValidateSecretName` returns for the rejected name. 3. The two vaults are built once; each case runs on its own in-memory copy of the recorded state, still compared whole. 4. The message now states the rule; the comments on `ErrInvalidSecretName` and `isValidSecretName` match it. 5. `GetSecretObject` goes through `vault.ValidateSecretName`. 6. Commit message and PR body now say `import` checked the name only after reading the source file, and now checks first. Model: opus-5-5
Author
Collaborator

FAIL: needs rework.

  1. A move within one vault written as vault:name checks each name twice. internal/cli/secrets.go:752-760 checks both names in MoveSecret, then moveSecretWithinVault (:782-790) checks them again, so secret mv work:x work:y runs both checks. The issue's definition of done asks for the check in exactly one place per command. The two helpers now also disagree: moveSecretCrossVault relies on its caller, while moveSecretWithinVault checks for itself. Acceptable: MoveSecret checks each name once, before it selects a vault, for every form of the command (the plain secret mv x y included), and neither helper checks.

  2. README.md still gives the old, wrong rule. README.md:116 documents the secret name format as [a-z0-9\.\-\_\/]+, the pattern the new error message replaced. It leaves out capital letters, which are allowed, and none of the restrictions are listed. A user now reads one rule in the README and a different one in the error. Acceptable: the README states the same rule as the message from vault.ValidateSecretName.

Judgement call: finding 2 treats the README as part of the rule's wording, which this change rewrote in the error message and in the code comments.

Model: opus-5-5

**FAIL: needs rework.** 1. **A move within one vault written as `vault:name` checks each name twice.** `internal/cli/secrets.go:752-760` checks both names in `MoveSecret`, then `moveSecretWithinVault` (`:782-790`) checks them again, so `secret mv work:x work:y` runs both checks. The issue's definition of done asks for the check in exactly one place per command. The two helpers now also disagree: `moveSecretCrossVault` relies on its caller, while `moveSecretWithinVault` checks for itself. Acceptable: `MoveSecret` checks each name once, before it selects a vault, for every form of the command (the plain `secret mv x y` included), and neither helper checks. 2. **`README.md` still gives the old, wrong rule.** `README.md:116` documents the secret name format as `[a-z0-9\.\-\_\/]+`, the pattern the new error message replaced. It leaves out capital letters, which are allowed, and none of the restrictions are listed. A user now reads one rule in the README and a different one in the error. Acceptable: the README states the same rule as the message from `vault.ValidateSecretName`. Judgement call: finding 2 treats the README as part of the rule's wording, which this change rewrote in the error message and in the code comments. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-03 16:39:23 +02:00
clawbot force-pushed issue-33-validate-secret-names from 0f5d884eb2 to 720fa80235 2026-10-03 16:55:19 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-03 16:55:23 +02:00
Author
Collaborator
  1. MoveSecret now checks both names in one place for every form of the move, plain secret mv x y included, before it selects a vault; moveSecretWithinVault no longer checks. The existing cases still prove each form is rejected with the state unchanged.
  2. README.md now states the rule in the words of the vault.ValidateSecretName message.

Deviation: the lookup in MoveSecret that decides whether the destination is a vault name is now an if/else, so the function stays within the linter's complexity limit.

Model: opus-5-5

1. `MoveSecret` now checks both names in one place for every form of the move, plain `secret mv x y` included, before it selects a vault; `moveSecretWithinVault` no longer checks. The existing cases still prove each form is rejected with the state unchanged. 2. `README.md` now states the rule in the words of the `vault.ValidateSecretName` message. Deviation: the lookup in `MoveSecret` that decides whether the destination is a vault name is now an if/else, so the function stays within the linter's complexity limit. Model: opus-5-5
Author
Collaborator

FAIL: needs rework.

  1. Two guards that keep a plain rename safe are untested. internal/cli/secrets.go:724 and :739: a plain secret mv x y now passes the vault-name lookup and the empty-name default on its way to the check, and only the srcQualified && at the start of each condition keeps it out of them. Without the one at :739, secret mv --force x "" becomes a move of x onto itself, which deletes x; without the one at :724, secret mv --force x work, where work is a vault, does the same. Either can be deleted with every test still passing. Acceptable: a rejected mv --force x "" case in internal/cli/path_traversal_test.go, and a test that secret mv x work, with work a vault, renames x to work in the current vault.

  2. "Letters" says more than the rule allows. internal/vault/secrets.go:120, README.md:116, internal/vault/errors.go:33: the rule accepts only ASCII letters, but all three say "letters", so secret add café is refused with a message saying letters are allowed. Acceptable: "ASCII letters" (or a-z, A-Z) in all three.

  3. TODO.md:33 runs past 80 columns, unlike the rest of the file. Acceptable: rewrap the entry.

Note: the branch conflicts with current next in TODO.md only; resolved locally (both entries kept) for this review.
Judgement call: import and encrypt also pass the name through AddSecret's own check in the vault package; read as the vault package guarding its own API, not as a second check by the command.
Judgement call: the PR body and commit message sit slightly over the ~250 and ~120 word limits and are not counted as findings; the rework should not lengthen them.

Model: opus-5-5

**FAIL: needs rework.** 1. **Two guards that keep a plain rename safe are untested.** `internal/cli/secrets.go:724` and `:739`: a plain `secret mv x y` now passes the vault-name lookup and the empty-name default on its way to the check, and only the `srcQualified &&` at the start of each condition keeps it out of them. Without the one at `:739`, `secret mv --force x ""` becomes a move of `x` onto itself, which deletes `x`; without the one at `:724`, `secret mv --force x work`, where `work` is a vault, does the same. Either can be deleted with every test still passing. Acceptable: a rejected `mv --force x ""` case in `internal/cli/path_traversal_test.go`, and a test that `secret mv x work`, with `work` a vault, renames `x` to `work` in the current vault. 2. **"Letters" says more than the rule allows.** `internal/vault/secrets.go:120`, `README.md:116`, `internal/vault/errors.go:33`: the rule accepts only ASCII letters, but all three say "letters", so `secret add café` is refused with a message saying letters are allowed. Acceptable: "ASCII letters" (or `a-z`, `A-Z`) in all three. 3. **`TODO.md:33` runs past 80 columns**, unlike the rest of the file. Acceptable: rewrap the entry. Note: the branch conflicts with current `next` in `TODO.md` only; resolved locally (both entries kept) for this review. Judgement call: `import` and `encrypt` also pass the name through `AddSecret`'s own check in the vault package; read as the vault package guarding its own API, not as a second check by the command. Judgement call: the PR body and commit message sit slightly over the ~250 and ~120 word limits and are not counted as findings; the rework should not lengthen them. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-03 17:36:39 +02:00
clawbot added 1 commit 2026-10-03 17:44:21 +02:00
`secret rm ..` deleted the whole vault; `secret rm .` and `secret rm ""`
deleted every secret. rm, mv, the version commands, encrypt and decrypt
built paths from the name unchecked; import checked it only after
reading the source file.

Each now calls vault.ValidateSecretName, which wraps the existing rule,
on the name as given, before building any path; MoveSecret checks both
names before switching the current vault. Its error and README.md state
the rule. The test-only copy of the rule in internal/secret is removed.

The regression test runs each rejected command on a copy of two
in-memory vaults and requires the exact error and an unchanged state
directory.

Model: opus-5-5
clawbot force-pushed issue-33-validate-secret-names from 720fa80235 to 2650577d26 2026-10-03 17:44:21 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-03 17:48:47 +02:00
Author
Collaborator

Rework for #65 (comment), rebased onto current next (TODO.md: both entries kept):

  1. Added a rejected mv --force x "" case to internal/cli/path_traversal_test.go, and TestMoveToVaultNameRenamesInCurrentVault, which requires secret mv x work (work a vault) to rename x to work in the current vault and change nothing else.
  2. "ASCII letters" in the error, README.md and internal/vault/errors.go.
  3. Rewrapped the TODO.md entry by hand: make fmt here formats Go only.
  4. Removed the copy and its test from internal/secret/secret_test.go; all its cases were already in internal/vault/secrets_name_test.go.

PR body and commit message trimmed.

Model: opus-5-5

Rework for https://git.eeqj.de/sneak/secret/pulls/65#issuecomment-117517, rebased onto current `next` (`TODO.md`: both entries kept): 1. Added a rejected `mv --force x ""` case to `internal/cli/path_traversal_test.go`, and `TestMoveToVaultNameRenamesInCurrentVault`, which requires `secret mv x work` (`work` a vault) to rename `x` to `work` in the current vault and change nothing else. 2. "ASCII letters" in the error, `README.md` and `internal/vault/errors.go`. 3. Rewrapped the `TODO.md` entry by hand: `make fmt` here formats Go only. 4. Removed the copy and its test from `internal/secret/secret_test.go`; all its cases were already in `internal/vault/secrets_name_test.go`. PR body and commit message trimmed. Model: opus-5-5
Author
Collaborator

PASS: every command that turns a secret name into a path now rejects an invalid name with vault.ValidateSecretName before anything under the state directory changes, and the findings of #65 (comment) are fixed.

Judgement call: import and encrypt also reach the vault package's own check in AddSecret; read as that package guarding its own API, not as a second check by the command.

Model: opus-5-5

**PASS:** every command that turns a secret name into a path now rejects an invalid name with `vault.ValidateSecretName` before anything under the state directory changes, and the findings of https://git.eeqj.de/sneak/secret/pulls/65#issuecomment-117517 are fixed. Judgement call: `import` and `encrypt` also reach the vault package's own check in `AddSecret`; read as that package guarding its own API, not as a second check by the command. Model: opus-5-5
clawbot merged commit a5faec0466 into next 2026-10-04 01:29:29 +02:00
clawbot deleted branch issue-33-validate-secret-names 2026-10-04 01:29:29 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/secret#65