Skip to content

Commit 8a18462

Browse files
committed
fix(shell): verify OMZ install script SHA256 before execution
1 parent 2f421b1 commit 8a18462

2 files changed

Lines changed: 130 additions & 6 deletions

File tree

internal/shell/shell.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
package shell
22

33
import (
4+
"crypto/sha256"
5+
"encoding/hex"
46
"fmt"
7+
"io"
8+
"net/http"
59
"os"
610
"os/exec"
711
"path/filepath"
812
"regexp"
913
"strings"
1014

15+
"github.com/openbootdotdev/openboot/internal/httputil"
1116
"github.com/openbootdotdev/openboot/internal/system"
1217
)
1318

19+
// knownOMZInstallHash is the SHA256 of the Oh-My-Zsh install script pinned on
20+
// 2026-04-19 (ohmyzsh/ohmyzsh master, commit circa that date). Update this
21+
// constant whenever the installer script changes upstream.
22+
const knownOMZInstallHash = "21043aec5b791ce4835479dc33ba2f92155946aeafd54604a8c83522627cc803"
23+
24+
// omzInstallURL is a var so tests can redirect it to a local httptest server.
25+
var omzInstallURL = "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh"
26+
1427
var shellIdentifierRe = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
1528

1629
func validateShellIdentifier(value, label string) error {
@@ -42,12 +55,53 @@ func InstallOhMyZsh(dryRun bool) error {
4255
return nil
4356
}
4457

45-
script := `sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended`
46-
cmd := exec.Command("bash", "-c", script)
47-
cmd.Stdout = os.Stdout
48-
cmd.Stderr = os.Stderr
49-
cmd.Stdin = os.Stdin
50-
return cmd.Run()
58+
// Download the installer via httputil.Do so rate-limit handling is applied.
59+
req, err := http.NewRequest(http.MethodGet, omzInstallURL, nil)
60+
if err != nil {
61+
return fmt.Errorf("create omz install request: %w", err)
62+
}
63+
resp, err := httputil.Do(http.DefaultClient, req)
64+
if err != nil {
65+
return fmt.Errorf("download omz install script: %w", err)
66+
}
67+
defer resp.Body.Close()
68+
69+
if resp.StatusCode != http.StatusOK {
70+
return fmt.Errorf("download omz install script: unexpected status %d", resp.StatusCode)
71+
}
72+
73+
scriptBytes, err := io.ReadAll(resp.Body)
74+
if err != nil {
75+
return fmt.Errorf("read omz install script: %w", err)
76+
}
77+
78+
// Verify SHA256 before executing anything.
79+
sum := sha256.Sum256(scriptBytes)
80+
got := hex.EncodeToString(sum[:])
81+
if got != knownOMZInstallHash {
82+
return fmt.Errorf("Oh-My-Zsh install script hash mismatch: download may be compromised (got %s, want %s)", got, knownOMZInstallHash)
83+
}
84+
85+
// Write verified script to a temp file, execute, then clean up.
86+
tmpFile, err := os.CreateTemp("", "omz-install-*.sh")
87+
if err != nil {
88+
return fmt.Errorf("create temp file for omz install: %w", err)
89+
}
90+
defer os.Remove(tmpFile.Name())
91+
92+
if _, err := tmpFile.Write(scriptBytes); err != nil {
93+
tmpFile.Close()
94+
return fmt.Errorf("write omz install script: %w", err)
95+
}
96+
if err := tmpFile.Close(); err != nil {
97+
return fmt.Errorf("close omz install script: %w", err)
98+
}
99+
100+
if err := os.Chmod(tmpFile.Name(), 0700); err != nil {
101+
return fmt.Errorf("chmod omz install script: %w", err)
102+
}
103+
104+
return system.RunCommand(tmpFile.Name(), "--unattended")
51105
}
52106

53107
const brewShellenvLine = `eval "$(/opt/homebrew/bin/brew shellenv)"`

internal/shell/shell_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package shell
22

33
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
49
"os"
510
"path/filepath"
611
"strings"
@@ -206,3 +211,68 @@ source $ZSH/oh-my-zsh.sh
206211
assert.Contains(t, string(content), `ZSH_THEME="agnoster"`)
207212
assert.NotContains(t, string(content), `ZSH_THEME="robbyrussell"`)
208213
}
214+
215+
// --- Hash verification tests for InstallOhMyZsh ---
216+
217+
// hashOf returns the lowercase hex SHA256 of data.
218+
func hashOf(data []byte) string {
219+
sum := sha256.Sum256(data)
220+
return hex.EncodeToString(sum[:])
221+
}
222+
223+
// serveScript starts an httptest server that returns body as the script payload.
224+
// It restores omzInstallURL when the test completes.
225+
func serveScript(t *testing.T, body []byte, statusCode int) {
226+
t.Helper()
227+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
228+
w.WriteHeader(statusCode)
229+
_, _ = w.Write(body)
230+
}))
231+
t.Cleanup(srv.Close)
232+
233+
orig := omzInstallURL
234+
omzInstallURL = srv.URL
235+
t.Cleanup(func() { omzInstallURL = orig })
236+
}
237+
238+
func TestInstallOhMyZsh_HashMismatch(t *testing.T) {
239+
home := t.TempDir()
240+
t.Setenv("HOME", home)
241+
242+
// Serve a script whose hash does NOT match knownOMZInstallHash.
243+
fakeScript := []byte("#!/bin/sh\necho fake\n")
244+
serveScript(t, fakeScript, http.StatusOK)
245+
246+
err := InstallOhMyZsh(false)
247+
require.Error(t, err)
248+
assert.Contains(t, err.Error(), "hash mismatch")
249+
assert.Contains(t, err.Error(), "download may be compromised")
250+
// The returned hash should be the hash of the fake content.
251+
assert.Contains(t, err.Error(), hashOf(fakeScript))
252+
}
253+
254+
func TestInstallOhMyZsh_HTTPError(t *testing.T) {
255+
home := t.TempDir()
256+
t.Setenv("HOME", home)
257+
258+
// Serve a 500 so the status-code check fires.
259+
serveScript(t, nil, http.StatusInternalServerError)
260+
261+
err := InstallOhMyZsh(false)
262+
require.Error(t, err)
263+
assert.Contains(t, err.Error(), fmt.Sprintf("unexpected status %d", http.StatusInternalServerError))
264+
}
265+
266+
func TestInstallOhMyZsh_DryRun_NoNetwork(t *testing.T) {
267+
home := t.TempDir()
268+
t.Setenv("HOME", home)
269+
270+
// Dry-run must return without making any network call — URL is deliberately
271+
// set to something unreachable to catch any accidental HTTP access.
272+
orig := omzInstallURL
273+
omzInstallURL = "http://127.0.0.1:0/should-not-be-called"
274+
t.Cleanup(func() { omzInstallURL = orig })
275+
276+
err := InstallOhMyZsh(true)
277+
assert.NoError(t, err)
278+
}

0 commit comments

Comments
 (0)