Skip to content

harden sandbox-name handling and Bluefile grammar validation - #1

Merged
chxmxii merged 1 commit into
chxperiments:mainfrom
KoLDXr00T:hardening/name-and-spec-validation
Aug 23, 2026
Merged

harden sandbox-name handling and Bluefile grammar validation#1
chxmxii merged 1 commit into
chxperiments:mainfrom
KoLDXr00T:hardening/name-and-spec-validation

Conversation

@KoLDXr00T

Copy link
Copy Markdown
Contributor

Summary

Two hardening changes from a source security review (commit c017e1b). The microVM boundary is left untouched — both fixes are host-side, at the points where inputs enter the system.

1. Sandbox names are validated once, in one place

internal/sandbox now owns a ValidName check ([A-Za-z0-9][A-Za-z0-9._-]{0,63}), enforced by the three filesystem path builders (Dir, DataDir, LogPath). Every destructive sink — destroy --data and reset (os.RemoveAll), rename (os.Rename) — routes through those builders, so a crafted name can no longer escape ~/.bluebox:

bluebox destroy ../../../.config --data -y   # previously deleted outside the bluebox root
bluebox new ../../somewhere                  # previously scaffolded directories anywhere

The check lives where paths are built rather than per command, so future commands inherit it instead of having to remember it.

2. Bluefile data fields can no longer change Containerfile structure

Spec fields that are data were interpolated raw into the generated Containerfile and its RUN shell lines, so they doubled as build grammar:

  • an env value containing \n became an extra instruction (ENV A=one + RUN curl attacker…)
  • write_files[].mode: "0755; curl …|sh" ran as a second shell command during build
  • blueprint user names/shells and package tokens had the same authority

These are now rejected at parse time with errors naming field and index, matching the existing fail-loudly philosophy (unknown keys already error). Render stays a pure function; escaping logic was deliberately not added.

Compatibility

  • Names containing /, leading ., or >64 chars stop working — such names never behaved coherently (nested dirs, inconsistent listing).
  • Specs relying on newline-in-env to add build steps stop parsing; fix is mechanical (move steps to run:).
  • All shipped examples/*/Bluefile still parse — enforced by a new regression test globbing examples/*/Bluefile.

Tests

  • internal/sandbox: table tests for ValidName; assertions that all three builders reject traversal names.
  • internal/bluefile: seven new grammar-rejection cases (newline env value, bad env key, non-octal mode, user name with space, relative shell, package injection token, base with whitespace) plus the examples regression test.
  • go build, go vet, gofmt, and go test ./... all clean.

Full analysis, options considered, tradeoffs, and implementation plans are in the accompanying hardening portfolio (proposals under sandbox-name-boundary and containerfile-spec-injection).

Two changes from a source security review (analysis in
bluebox-hardening/):

1. Sandbox names are now validated once, inside the sandbox package, by
   ValidName (letters/digits/._- , max 64, alphanumeric first char).
   Dir/DataDir/LogPath enforce it, so no command -- including destroy
   --data, reset and rename, which run os.RemoveAll/os.Rename on
   name-derived paths -- can act outside BLUEBOX_HOME via a crafted
   name like "../../something".

2. Bluefile fields that are data, not instructions, are validated at
   parse time so the generated Containerfile is a faithful rendering of
   the parsed spec: env keys must be identifiers and values cannot
   contain newlines (a newline would inject a Containerfile
   instruction), write_files modes must be octal (a mode like
   "0755; curl ...|sh" ran as a second shell command), blueprint user
   names/shells are constrained, packages must be plain tokens, and
   base must be whitespace-free.

All shipped examples still parse; both packages gain table-driven
regression tests.
@chxmxii

chxmxii commented Aug 23, 2026

Copy link
Copy Markdown
Member

Your agent is so bad!

@chxmxii chxmxii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — it's a well-crafted PR. Putting ValidName at the path builders so future commands inherit it, rejecting bad grammar at parse time with field/index in the message, keeping Render pure, and the examples/*/Bluefile regression test are all exactly the right calls. I checked it out and confirmed go build/vet/gofmt/test are clean and the traversal-name protection blocks destroy ../../../.config.

One blocking issue before merge, and it's in the same class the PR closes.

write_files[].path isn't validated, and it reaches a RUN line

path is only checked for a leading /, but it's interpolated into RUN chmod <mode> <path>. So it doubles as build grammar the same way mode did. This Bluefile parses cleanly:

base: docker.io/library/alpine:latest
blueprint:
  write_files:
    - path: "/tmp/x; touch /tmp/PWNED # "
      content: "hi"
      mode: "0644"

and Containerfile() renders:

COPY .blueprint/f0 /tmp/x; touch /tmp/PWNED #
RUN chmod 0644 /tmp/x; touch /tmp/PWNED #

so touch /tmp/PWNED runs at build time. It triggers only when mode is set (that's the one line that shell-interpolates the path; the COPY line isn't shell), but the vector is real.

The fix fits the pattern you already established — a path regex alongside the write_files loop, mirroring your shellRe:

pathRe = regexp.MustCompile(`^/[A-Za-z0-9/._-]+$`)
if !pathRe.MatchString(f.Path) {
    return fmt.Errorf("blueprint.write_files[%d]: path must be an absolute path (letters, digits, '/', '.', '_', '-'), got %q", i, f.Path)
}

That also subsumes the existing leading-/ check, and a rejection test case in the same table as the others would round it out.

Non-blocking nit

SnapshotsDir doesn't call ValidName itself. It's safe today because every caller reaches it after DataDir/Dir has already validated, but a direct ValidName there would match the "validate where the path is built" principle the rest of the PR follows.

Everything else looks great — fix the path validation and I'll merge.

@chxmxii chxmxii added enhancement New feature or request good first issue Good for newcomers labels Aug 23, 2026
@chxmxii chxmxii added this to the v0.2.1 — Input hardening milestone Aug 23, 2026
@chxmxii

chxmxii commented Aug 23, 2026

Copy link
Copy Markdown
Member

Merging this now — the name and grammar validation is solid and I'd rather land it than hold it. I'll take the two follow-ups myself in a separate commit so you don't have to round-trip: the write_files[].path regex (the blocking item above) and the ValidName call in SnapshotsDir (the nit). Both are on the v0.2.1 milestone. Thanks for the careful work — the "validate where the path is built" approach is exactly the pattern the follow-ups will extend.

@chxmxii
chxmxii merged commit 156b9ca into chxperiments:main Aug 23, 2026
1 check passed
chxmxii added a commit that referenced this pull request Aug 23, 2026
Two follow-ups to #1, completing the v0.2.1 input-hardening pass.

write_files[].path was only checked for a leading slash, but it is interpolated
into a COPY and, when a mode is set, into a RUN chmod line -- so it is build
grammar the same way mode is. A path like "/tmp/x; touch /PWNED # " passed
validation and injected a second shell command at build time. It now must match
the same absolute-path grammar as a user shell.

SnapshotsDir did not call ValidName, unlike the other path builders. It was safe
in practice because every caller reached it after Dir/DataDir had validated, but
validating where the path is built removes the standing assumption.

Closes #2
Closes #3
KoLDXr00T added a commit to KoLDXr00T/bluebox that referenced this pull request Aug 23, 2026
Address review on chxperiments#1:

- blueprint.write_files[].path reaches `RUN chmod <mode> <path>`, so a
  path like "/tmp/x; touch /tmp/PWNED #" ran as a second build command.
  It is now constrained to plain absolute paths by pathRe, which
  subsumes the old leading-"/" check. Regression test added.

- SnapshotsDir now calls ValidName itself, matching the "validate where
  the path is built" rule the other path builders follow.
chxmxii added a commit that referenced this pull request Aug 23, 2026
Add a mounts: list so the host state a sandbox can touch is explicit in the
spec rather than the single implicit /data share.

  mounts:
    - host: ~/projects/demo
      guest: /work
      mode: ro        # ro (default) or rw

Every field is validated and normalized at parse time (the fail-loudly pattern
from #1): host must be absolute (~ expands to home) and free of ':', guest
matches the absolute-path grammar and cannot shadow /data, mode is ro|rw and
defaults to the fail-safe ro, and guest targets must be unique. At run time each
mount becomes a -v host:guest:mode after the /data share, which vmArgs already
builds in one place.

Closes #5

Co-authored-by: chxmxii <mouhib2000@yahoo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request good first issue Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants