Skip to content

fix(mempalace): validate Kind/Tenant/Key against path traversal#5

Merged
tzone85 merged 1 commit into
mainfrom
harden/mempalace-input-validation
Jun 11, 2026
Merged

fix(mempalace): validate Kind/Tenant/Key against path traversal#5
tzone85 merged 1 commit into
mainfrom
harden/mempalace-input-validation

Conversation

@tzone85

@tzone85 tzone85 commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Write, Read, List now validate every caller-supplied path component (Kind, Tenant, Key) against a strict regex before touching the filesystem. Violations return wrapped ErrInvalidInput.
  • The pre-change // safe key check comment in Read was aspirational — there was no actual check. Test proved an attacker who reached the package-level API (or a future caller that forgot to pre-validate) could escape the wing trivially with Kind: ".." or an explicit Key: "../escape".

In practice the only current caller is advise_cmd with hardcoded kind="decision"/"bom" and an already-validated tenant, so this gate is preventative rather than reactive. But mempalace is a public package-level API — the new caller in 6 months won't know about the implicit pre-validation contract.

Grammar:

  • Kind — ^[a-z][a-z0-9-]{0,31}$
  • Tenant — same DNS-label grammar as tenant.New()
  • Key (when explicit) — must be 64 lowercase hex chars (the content-addressed shape Write computes internally)

TDD

RED: TestWrite_RejectsTraversalShapedInputs + TestRead_RejectsTraversalShapedInputs — 15 sub-tests across .., slash, uppercase, non-hex. All 15 failed before the fix — many actually wrote files outside the wing dir.

GREEN: added regex validators + checks at every public entry point. All 15 sub-tests pass.

Test plan

  • go test -race ./... — all packages green
  • bash scripts/cover_check.sh — PASS, mempalace 90.5% (target 87)
  • golangci-lint run — 0 issues

Write/Read/List all join caller-supplied strings into the on-disk
path under WingDir(tenant)/kind/<key>.json. The pre-change "safe
key check" comment in Read was aspirational — there was no actual
check.

In practice Write is only called from `advise_cmd` with hardcoded
kind="decision" or "bom" and an already-validated tenant, so this
gate is preventative. But the Bridge is a public package-level API
and the test surface proved an attacker who could call it directly
(or a future caller that forgets to pre-validate) could escape the
wing trivially:

  Drawer{Kind: "..", Tenant: "acme", Body: ...}
  → wrote to tenants/acme/mempalace-wing/../<sha>.json
  Drawer{Kind: "decision", Tenant: "acme", Key: "../escape", ...}
  → wrote to tenants/acme/mempalace-wing/decision/../escape.json

Now:
- Kind must match `^[a-z][a-z0-9-]{0,31}$`
- Tenant must match the same DNS-label grammar tenant.New() enforces
- Key (when supplied explicitly) must be 64 lowercase hex chars —
  the content-addressed shape Write computes internally
All three components are checked on every public entry point
(Write, Read, List) and the violation returns wrapped ErrInvalidInput
so callers can distinguish input rejection from FS-side failure.

The List check also lets us drop the slightly suspicious legacy
`.json`-suffix-strip logic from "this might be safe" to "we know
the key was already in our grammar".

Coverage: mempalace 90.5% (target 87), global PASS.
@tzone85 tzone85 merged commit 68e5810 into main Jun 11, 2026
2 checks passed
@tzone85 tzone85 deleted the harden/mempalace-input-validation branch June 11, 2026 09:20
tzone85 added a commit that referenced this pull request Jun 11, 2026
Adds an Unreleased section to CHANGELOG.md covering the five
sequential PRs that landed today:
- #2 chore(security): bump x/sys for GO-2026-5024
- #3 fix(api): validate tenant id at route boundary
- #4 fix(ingest): reject option-shaped --base-ref
- #5 fix(mempalace): validate Kind/Tenant/Key against path traversal
- #6 chore(supply-chain): pin govulncheck + base images by digest

Updates README Status to mention the hardening pass and replaces a
stale `§9` design-spec cross-reference with the section's actual
name (per the project's doc-style rule against the § symbol).

No code changes, no behaviour changes — pure documentation refresh
for the audit story.
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