diff --git a/SHELL_FEATURES.md b/SHELL_FEATURES.md index c96165ca..9ac4a531 100644 --- a/SHELL_FEATURES.md +++ b/SHELL_FEATURES.md @@ -66,7 +66,7 @@ Blocked features are rejected before execution with exit code 2. - ✅ Line continuation: `\` at end of line - ✅ Comments: `# text` - ❌ Extended globbing: `@(pat)`, `*(pat)`, etc. -- ❌ Tilde expansion: `~`, `~/path` +- ❌ Tilde expansion: `~`, `~/path`, `~user` - ❌ Process substitution: `<(cmd)`, `>(cmd)` ## Execution diff --git a/interp/validate.go b/interp/validate.go index 9a5e9232..f9d4b6da 100644 --- a/interp/validate.go +++ b/interp/validate.go @@ -5,6 +5,7 @@ package interp import ( "fmt" + "strings" "mvdan.cc/sh/v3/syntax" ) @@ -14,6 +15,7 @@ import ( // so that disallowed features are caught early with a clear error message. func validateNode(node syntax.Node) error { var err error + hdocWords := make(map[*syntax.Word]bool) syntax.Walk(node, func(n syntax.Node) bool { if err != nil { return false @@ -110,6 +112,21 @@ func validateNode(node syntax.Node) error { if err != nil { return false } + if n.Hdoc != nil { + hdocWords[n.Hdoc] = true + } + + // Blocked tilde expansion (prevents host user info disclosure via os/user.Lookup). + // Heredoc bodies are excluded since they don't undergo tilde expansion. + case *syntax.Word: + if !hdocWords[n] && len(n.Parts) > 0 { + if lit, ok := n.Parts[0].(*syntax.Lit); ok { + if strings.HasPrefix(lit.Value, "~") { + err = fmt.Errorf("tilde expansion is not supported") + return false + } + } + } } return true }) @@ -119,9 +136,9 @@ func validateNode(node syntax.Node) error { // blockedSpecialParams are single-character parameter names that are not // supported in the safe-shell interpreter (positional params, $#, $0, $@, $*). var blockedSpecialParams = map[string]bool{ - "#": true, // $# - number of positional parameters - "!": true, // $! - PID of the last background command - "0": true, // $0 - name of the shell or script + "#": true, // $# - number of positional parameters + "!": true, // $! - PID of the last background command + "0": true, // $0 - name of the shell or script "1": true, "2": true, "3": true, "4": true, // $1-$9 - positional parameters "5": true, "6": true, "7": true, "8": true, "9": true, "@": true, // $@ - all positional parameters as separate words diff --git a/tests/scenarios/shell/environment/tilde_in_heredoc_allowed.yaml b/tests/scenarios/shell/environment/tilde_in_heredoc_allowed.yaml new file mode 100644 index 00000000..8f4e5535 --- /dev/null +++ b/tests/scenarios/shell/environment/tilde_in_heredoc_allowed.yaml @@ -0,0 +1,14 @@ +test_against_local_shell: false +description: Tilde inside heredoc body is allowed (heredocs do not undergo tilde expansion). +input: + script: |+ + cat <