Skip to content

Commit ba5f1b1

Browse files
alecthomascodex
andcommitted
fix: reserve Hermit execution environment variables
Manifest-controlled ACTIVE_HERMIT and XDG_CACHE_HOME can redirect Hermit to attacker-selected binaries, bypassing script verification. Reject both variables from manifests and cover the behavior in unit and shell integration tests. Co-authored-by: Codex <noreply@openai.com>
1 parent 95acc42 commit ba5f1b1

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

envars/util.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ var validEnvKey = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
2020

2121
var reservedEnvKey = regexp.MustCompile(`^_?HERMIT_`)
2222

23+
var reservedManifestEnvKeys = map[string]bool{
24+
// These variables do not use Hermit's prefix, but Hermit's shell scripts
25+
// and bootstrap consume them to select the binary to execute.
26+
"ACTIVE_HERMIT": true,
27+
"XDG_CACHE_HOME": true,
28+
}
29+
2330
// ValidateKey returns an error if key is not a valid POSIX environment
2431
// variable name. Hermit emits keys verbatim into shell scripts, so anything
2532
// outside this pattern is rejected to prevent shell command injection.
@@ -34,8 +41,8 @@ func ValidateManifestKey(key string) error {
3441
if err := ValidateKey(key); err != nil {
3542
return err
3643
}
37-
if reservedEnvKey.MatchString(key) {
38-
return errors.Errorf("environment variable name %q is reserved by Hermit (names prefixed with HERMIT_ or _HERMIT_ cannot be set from a manifest)", key)
44+
if reservedEnvKey.MatchString(key) || reservedManifestEnvKeys[key] {
45+
return errors.Errorf("environment variable name %q is reserved by Hermit and cannot be set from a manifest", key)
3946
}
4047
return nil
4148
}

envars/util_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ func TestValidateKey(t *testing.T) {
131131
func TestEnvarsValidate(t *testing.T) {
132132
assert.NoError(t, Envars{"FOO": "1", "BAR_2": "2"}.Validate())
133133
assert.Error(t, Envars{"FOO": "1", "EVIL; touch /tmp/x; X": "v"}.Validate())
134-
assert.NoError(t, Envars{"HERMIT_EXE": "/tmp/evil"}.Validate())
134+
assert.NoError(t, Envars{
135+
"HERMIT_EXE": "/tmp/evil",
136+
"ACTIVE_HERMIT": "/tmp/evil",
137+
"XDG_CACHE_HOME": "/tmp/evil",
138+
}.Validate())
135139
}
136140

137141
func TestValidateManifestKey(t *testing.T) {
@@ -142,7 +146,8 @@ func TestValidateManifestKey(t *testing.T) {
142146
}{
143147
{name: "Simple", key: "FOO", wantErr: false},
144148
{name: "MentionsHermit", key: "MY_HERMIT_THING", wantErr: false},
145-
{name: "ActiveHermit", key: "ACTIVE_HERMIT", wantErr: false},
149+
{name: "ReservedActiveHermit", key: "ACTIVE_HERMIT", wantErr: true},
150+
{name: "ReservedXDGCacheHome", key: "XDG_CACHE_HOME", wantErr: true},
146151
{name: "ReservedExe", key: "HERMIT_EXE", wantErr: true},
147152
{name: "ReservedDistURL", key: "HERMIT_DIST_URL", wantErr: true},
148153
{name: "ReservedStateDir", key: "HERMIT_STATE_DIR", wantErr: true},
@@ -167,6 +172,8 @@ func TestEnvarsValidateManifest(t *testing.T) {
167172
assert.NoError(t, Envars{"FOO": "1", "BAR_2": "2"}.ValidateManifest())
168173
assert.Error(t, Envars{"FOO": "1", "HERMIT_EXE": "/tmp/evil"}.ValidateManifest())
169174
assert.Error(t, Envars{"_HERMIT_OLD_PATH": "x"}.ValidateManifest())
175+
assert.Error(t, Envars{"ACTIVE_HERMIT": "/tmp/evil"}.ValidateManifest())
176+
assert.Error(t, Envars{"XDG_CACHE_HOME": "/tmp/evil"}.ValidateManifest())
170177
}
171178

172179
func TestExpandDateTime(t *testing.T) {

integration/integration_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,24 @@ EOF
134134
assert test -z "${HERMIT_ROOT_BIN:-}"
135135
`,
136136
},
137+
{
138+
// Regression test for DX-28: unprefixed variables consumed by
139+
// Hermit's shell scripts and bootstrap must be reserved too.
140+
name: "ManifestCannotSetHermitExecutionEnvars",
141+
script: `
142+
hermit init .
143+
for name in ACTIVE_HERMIT XDG_CACHE_HOME; do
144+
cat > bin/hermit.hcl <<EOF
145+
env = {
146+
"$name": "/tmp/evil",
147+
}
148+
EOF
149+
assert test "$(hermit validate env . >/dev/null 2>&1; echo $?)" != "0"
150+
done
151+
. bin/activate-hermit >/dev/null 2>&1 || true
152+
assert test "${XDG_CACHE_HOME:-}" != "/tmp/evil"
153+
`,
154+
},
137155
{
138156
// Regression test for VULN-78247: RCE via an unvalidated git transport scheme.
139157
name: "MaliciousGitSourceSchemeDoesNotExecute",

0 commit comments

Comments
 (0)