hosystem Engagements

ho-04.9 — identifier & path validation

created 2026-07-03
status complete
type ho-document
project sharibako
ho 04.9
kamae 5
shape ri

Second post-sweep hardening (fable sweep, 2026-07-03; Tier B candidate #3). Closes the traversal class: scope IDs, keys, shared-entry IDs, .link payloads, and .sharibako marker fields all sync via git from other machines and all became path components with no . A pre-session scout confirmed — and sharpened — the sweep's finding: clean would removeItem() whatever path a crafted materialize_to resolved to (arbitrary file deletion, not just writes), absolute targets were honored outright, and a tampered .link payload aimed rotateShared's re-encrypt at any path.

Authored light per the FABLE 2 planning session; the findings document and scout report scoped the work.

Out of scope:


Decisions

  1. One identifier grammar everywhere: ^[A-Za-z0-9_][A-Za-z0-9._-]*$. Scope IDs, keys, shared-entry IDs, and .link payloads share it. The alphabet has no / and the first-character class excludes ., so ./.., hidden-file names, and ho-04.8's .sharibako-tmp- staging prefix are structurally impossible rather than special-cased. Env-style uppercase keys and the ingest sanitizer's lowercase-dash IDs both conform — existing vaults pass unchanged.

  2. VaultLayout is the throwing chokepoint. Every ID-taking URL helper validates before the ID becomes a path component (invalidIdentifier(kind:value:source:), a new VaultError case carrying the originating file when the value arrived via vault data). Boundaries also validate early for better errors: loadMarker (scope field → markerMalformed), readLinkTarget (payload, with the .link file as source), link() (write side of the payload contract), InitCommand's scope-ID prompt (re-prompt loop; the suggested default is sanitizer output and always valid). VaultCore.isValidIdentifier is the public gateway for surfaces.

  3. Listing verbs skip, they don't throw. listScopes / listShared / inspect / linkGraph silently exclude out-of-grammar names read from disk — one stray directory must not brick status.

  4. materialize_to containment: relative-only, inside the marker's subtree. ScopeMarker.validatedTargetURL() rejects absolute, ~-prefixed, and (post-standardization) escaping targets; materialize/clean/heal/update consume it, and loadMarker fails hostile markers at load so they surface on scan/status before anything acts. The raw targetURL property remains for display. A legitimate cross-directory materialize becomes a deliberate future opt-in, not a default.

  5. Remote-URL transport allowlist, defense-in-depth. setRemote isn't reachable from git-synced data today (no CLI verb wires it), but git transport helpers (ext::<cmd>) turn a remote string into command execution, so the safe property is made structural: allow https://, ssh://, file://, absolute local paths (test remotes), and scp-style user@host:path; reject transport helpers, git://, http://, and relative paths (remoteURLRejected).

Verification

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ a97b22af9b61 · ho-system @ 79e96b801a13 · the glossary · the colophon