Skip to content

Resolve ls paths in safe's own syntax - #28

Merged
wayneeseguin merged 3 commits into
developfrom
ls-escaped-paths
Jul 29, 2026
Merged

Resolve ls paths in safe's own syntax#28
wayneeseguin merged 3 commits into
developfrom
ls-escaped-paths

Conversation

@wayneeseguin

Copy link
Copy Markdown
Contributor

safe ls was the last listing command that did not speak safe's own path
syntax. It passed its argument straight to the list call, which talks to Vault
in literal paths, and joined that raw argument to each child before reading it
back.

What that cost

Fixture: one secret at the literal path secret/od:d/inner, on the KV v2
secret/ mount of a real Vault 1.13.2 dev server.

$ safe paths secret
secret/od\:d/inner
secret/plain/inner

Before:

$ safe ls 'secret/od\:d'          # safe's own output, pasted back
!! no secret exists at path `secret/od\:d`
rc=1

$ safe ls 'secret/od:d'           # the literal name
                                  # <- nothing
rc=0

$ safe ls --quick 'secret/od:d'
inner
rc=0

The second one is the bad one. inner exists and is alive; the listing is
empty and the exit code is 0. The liveness check on a version 2 mount escapes
only the child before handing the path to Read, so secret/od:d + /inner
arrives as secret/od:d/inner, Read splits it at the colon into the path
secret/od and the key d/inner, misses, and continues past the entry.
--quick skips that check, which is why it prints the secret the default
listing hides.

After:

$ safe ls 'secret/od\:d'
inner

$ safe ls 'secret/od:d'
!! ls does not take a specific key (secret/od:d)
rc=1

$ safe ls secret
od\:d/  plain/

The change

Three parts, one behaviour:

  • Resolve the root through walkRoot, the same helper tree, paths, and
    values have used since Accept safe's own escaped paths as input #15. A key or a version cannot scope a listing, so
    naming one is refused rather than looked up as part of the path.

  • Escape the whole path, not just the child, before handing it to Read; the
    mount-version lookup keeps the literal form, which is what it wants.

  • Escape the names ls prints, for the same reason paths and tree escape
    theirs. Without this the fix is only half a round trip: ls secret would
    print a name that ls itself now refuses.

Plain names are untouched — EscapePathSegment is the identity on anything
without a \, :, or ^.

Tests

internal/cli/ls_paths_test.go, seven tests over both mount versions:
escaped root accepted on v1 and v2, the quiet-drop regression (the default
listing must agree with --quick), printed names escaped, output fed back in,
plain listings byte-identical in both -1 and default form, and key/version
refused.

Mutation-tested on each axis independently, against a clean tree:

Reverted Tests that fail
root resolution 5 — everything but the plain-listing and printed-names tests
child escaping 3 — the two v2 tests and the round trip; the v1 test survives, since a v1 mount never runs the liveness check
printed-name escaping 2 — printed names and the round trip

make check and go test -race ./... green. Every before/after pair above is
from a live Vault, not the fake.

Docs

ls joins the path-only bullet in the Secret Paths rules, the escaping section
gains a short note on joining a printed name to the path you listed, and the
command reference gains an ### ls section — it had none, though ls is one
of the first commands anybody reaches for. Both examples were run against the
live server before being written down.

safe prints paths escaped, but ls passed its argument straight to the
list call, which talks to Vault in literal paths, and joined that raw
argument to each child before reading it back. A folder whose name
holds a colon was unreachable from safe's own output, and reachable
in a way that lied from the literal name: the liveness check on a
version 2 mount parsed the colon as a key separator, read a shorter
path, missed, and dropped every child from the listing -- an empty
listing at exit 0.

Resolve the root the way the tree walks do, and escape the whole path
rather than only the child before handing it to Read. A key or a
version cannot scope a listing, so naming one is refused rather than
looked up as part of the path.

The names ls prints are escaped for the same reason paths and tree
escape theirs: what safe prints has to be what safe accepts.
safe ls now resolves its argument like the other listings, so the
path syntax rules cover it: it takes a path only, and the names it
prints come back escaped.
The command reference skipped ls entirely, though it is one of the
first commands anybody reaches for.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant