Files

129 lines
3.2 KiB
Go

// Package securityutil provides security-related utilities for command execution,
// input validation, and sanitization.
package securityutil
import (
"fmt"
"strings"
)
// ShellEscape escapes a string for safe use in shell commands.
// Returns the string wrapped in single quotes with internal single quotes escaped.
// If the string contains no special characters, it's returned as-is for readability.
func ShellEscape(s string) string {
if s == "" {
return "''"
}
// If no special characters, return as-is
if !strings.ContainsAny(s, " \t\n'\"\\$`!*?[](){};<>|&") {
return s
}
// Single-quote and escape internal single quotes
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}
// SplitShellCommand splits a shell command string into arguments,
// respecting quoted strings and escape sequences.
// This is a simple shell-word parser that handles:
// - Single quotes (no escaping inside)
// - Double quotes (backslash escaping)
// - Backslash escaping outside quotes
// - Whitespace as argument separator
func SplitShellCommand(cmd string) ([]string, error) {
state := &parseState{
args: make([]string, 0),
current: &strings.Builder{},
}
for i, r := range cmd {
if state.escape {
state.current.WriteRune(r)
state.escape = false
continue
}
if !handleSpecialChar(r, state) {
state.current.WriteRune(r)
}
// Check for unclosed quotes at end
if i == len(cmd)-1 && (state.inSingleQuote || state.inDoubleQuote) {
return nil, fmt.Errorf("unclosed quote in command")
}
}
if state.current.Len() > 0 {
state.args = append(state.args, state.current.String())
}
return state.args, nil
}
// parseState tracks the current parsing state
type parseState struct {
inSingleQuote bool
inDoubleQuote bool
escape bool
args []string
current *strings.Builder
}
// handleSpecialChar processes special characters and returns true if handled
func handleSpecialChar(r rune, state *parseState) bool {
switch r {
case '\\':
return handleBackslash(state)
case '\'':
return handleSingleQuote(state)
case '"':
return handleDoubleQuote(state)
case ' ', '\t', '\n':
return handleWhitespace(state)
default:
return false
}
}
// handleBackslash processes backslash escape sequences
func handleBackslash(state *parseState) bool {
if state.inSingleQuote {
state.current.WriteRune('\\')
} else {
state.escape = true
}
return true
}
// handleSingleQuote processes single quote characters
func handleSingleQuote(state *parseState) bool {
if state.inDoubleQuote {
state.current.WriteRune('\'')
} else {
state.inSingleQuote = !state.inSingleQuote
}
return true
}
// handleDoubleQuote processes double quote characters
func handleDoubleQuote(state *parseState) bool {
if state.inSingleQuote {
state.current.WriteRune('"')
} else {
state.inDoubleQuote = !state.inDoubleQuote
}
return true
}
// handleWhitespace processes whitespace as argument separator
func handleWhitespace(state *parseState) bool {
if state.inSingleQuote || state.inDoubleQuote {
state.current.WriteRune(' ')
return true
}
if state.current.Len() > 0 {
state.args = append(state.args, state.current.String())
state.current.Reset()
}
return true
}