Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 32 additions & 18 deletions pkg/utils/security/escape.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,42 @@ import (
// a -> a
// a b -> a b
// $a -> $'$a'
// $'a' -> $'$\'$a'\'
// $'a' -> $'$\'a\”
func EscapeBashStr(s string) string {
if !containsOne(s, []rune{'$', '`', '&', ';', '>', '|', '(', ')'}) {
// Check if string contains any shell-sensitive characters that require escaping
// Added '\', '\'', '\n', '\r', and '\t' to the list as identified by security review
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

This comment is now out of sync with the code: the trigger list also includes a space (' '), but the comment only mentions adding backslash/quote/newline/CR/tab. Please update the comment to reflect the full set of newly included characters (or rephrase to avoid listing them explicitly).

Suggested change
// Added '\', '\'', '\n', '\r', and '\t' to the list as identified by security review
// Added '\', '\'', '\n', '\r', '\t', and space to the list as identified by security review

Copilot uses AI. Check for mistakes.
if !containsOne(s, []rune{'$', '`', '&', ';', '>', '|', '(', ')', '\'', '\\', '\n', '\r', '\t', ' '}) {
return s
}
s = strings.ReplaceAll(s, `\`, `\\`)
s = strings.ReplaceAll(s, `'`, `\'`)
if strings.Contains(s, `\\`) {
s = strings.ReplaceAll(s, `\\\\`, `\\`)
s = strings.ReplaceAll(s, `\\\'`, `\'`)
s = strings.ReplaceAll(s, `\\"`, `\"`)
s = strings.ReplaceAll(s, `\\a`, `\a`)
s = strings.ReplaceAll(s, `\\b`, `\b`)
s = strings.ReplaceAll(s, `\\e`, `\e`)
s = strings.ReplaceAll(s, `\\E`, `\E`)
s = strings.ReplaceAll(s, `\\n`, `\n`)
s = strings.ReplaceAll(s, `\\r`, `\r`)
s = strings.ReplaceAll(s, `\\t`, `\t`)
s = strings.ReplaceAll(s, `\\v`, `\v`)
s = strings.ReplaceAll(s, `\\?`, `\?`)

// Build the escaped string manually to handle all special cases correctly
var result strings.Builder

for _, ch := range s {
switch ch {
case '\\':
// Escape backslashes by doubling them
result.WriteString(`\\`)
case '\'':
// Escape single quotes
result.WriteString(`\'`)
case '\n':
// Preserve newline as literal \n in ANSI-C quoted string
result.WriteString(`\n`)
case '\r':
// Preserve carriage return as literal \r in ANSI-C quoted string
result.WriteString(`\r`)
case '\t':
// Preserve tab as literal \t in ANSI-C quoted string
Comment on lines +48 to +54
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

These inline comments are misleading: in bash ANSI-C quoting, \n, \r, and \t inside $'...' are interpreted as actual newline/CR/tab characters, not preserved as the two-character sequences. Consider rewording to something like “encode newline as \n escape” to avoid confusion.

Suggested change
// Preserve newline as literal \n in ANSI-C quoted string
result.WriteString(`\n`)
case '\r':
// Preserve carriage return as literal \r in ANSI-C quoted string
result.WriteString(`\r`)
case '\t':
// Preserve tab as literal \t in ANSI-C quoted string
// Encode newline using \n escape for ANSI-C quoted string
result.WriteString(`\n`)
case '\r':
// Encode carriage return using \r escape for ANSI-C quoted string
result.WriteString(`\r`)
case '\t':
// Encode tab using \t escape for ANSI-C quoted string

Copilot uses AI. Check for mistakes.
result.WriteString(`\t`)
default:
// All other characters (including $, `, &, etc.) are safe within $'...'
result.WriteRune(ch)
}
}
return fmt.Sprintf(`$'%s'`, s)

// Wrap in ANSI-C quoting format
return fmt.Sprintf(`$'%s'`, result.String())
}

func containsOne(target string, chars []rune) bool {
Expand Down
265 changes: 243 additions & 22 deletions pkg/utils/security/escape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,247 @@ limitations under the License.

package security

import "testing"

func TestEscapeBashStr(t *testing.T) {
cases := [][]string{
{"abc", "abc"},
{"test-volume", "test-volume"},
{"http://minio.kube-system:9000/minio/dynamic-ce", "http://minio.kube-system:9000/minio/dynamic-ce"},
{"$(cat /proc/self/status | grep CapEff > /test.txt)", "$'$(cat /proc/self/status | grep CapEff > /test.txt)'"},
{"hel`cat /proc/self/status`lo", "$'hel`cat /proc/self/status`lo'"},
{"'h'el`cat /proc/self/status`lo", "$'\\'h\\'el`cat /proc/self/status`lo'"},
{"\\'h\\'el`cat /proc/self/status`lo", "$'\\'h\\'el`cat /proc/self/status`lo'"},
{"$'h'el`cat /proc/self/status`lo", "$'$\\'h\\'el`cat /proc/self/status`lo'"},
{"hel\\`cat /proc/self/status`lo", "$'hel\\\\`cat /proc/self/status`lo'"},
{"hel\\\\`cat /proc/self/status`lo", "$'hel\\\\`cat /proc/self/status`lo'"},
{"hel\\'`cat /proc/self/status`lo", "$'hel\\'`cat /proc/self/status`lo'"},
}
for _, c := range cases {
escaped := EscapeBashStr(c[0])
if escaped != c[1] {
t.Errorf("escapeBashVar(%s) = %s, want %s", c[0], escaped, c[1])
}
}
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"testing"
Comment on lines +20 to +22
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The import block isn’t gofmt-compliant: standard library imports ("testing") should be grouped before third-party imports, with a blank line between groups. Please run gofmt (or reorder imports) to match the repository’s formatting conventions.

Suggested change
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"testing"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

Copilot uses AI. Check for mistakes.
)

func TestSecurity(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Security Suite")
}

var _ = Describe("EscapeBashStr", func() {
Context("when string contains no special characters", func() {
It("should return the original string for simple alphanumeric", func() {
result := EscapeBashStr("abc")
Expect(result).To(Equal("abc"))
})

It("should return the original string with hyphens", func() {
result := EscapeBashStr("test-volume")
Expect(result).To(Equal("test-volume"))
})

It("should return the original string for URLs without special chars", func() {
result := EscapeBashStr("http://minio.kube-system:9000/minio/dynamic-ce")
Expect(result).To(Equal("http://minio.kube-system:9000/minio/dynamic-ce"))
})

It("should return the original string with underscores and dots", func() {
result := EscapeBashStr("test_file.txt")
Expect(result).To(Equal("test_file.txt"))
})

It("should return the original string with slashes", func() {
result := EscapeBashStr("/path/to/file")
Expect(result).To(Equal("/path/to/file"))
})
})

Context("when string contains command substitution", func() {
It("should escape dollar sign with parentheses", func() {
result := EscapeBashStr("$(cat /proc/self/status | grep CapEff > /test.txt)")
Expect(result).To(Equal("$'$(cat /proc/self/status | grep CapEff > /test.txt)'"))
})

It("should escape backticks", func() {
result := EscapeBashStr("hel`cat /proc/self/status`lo")
Expect(result).To(Equal("$'hel`cat /proc/self/status`lo'"))
})

It("should escape multiple command substitution attempts", func() {
result := EscapeBashStr("`whoami` and `date`")
Expect(result).To(Equal("$'`whoami` and `date`'"))
})
})

Context("when string contains quotes", func() {
It("should escape single quotes with backticks", func() {
result := EscapeBashStr("'h'el`cat /proc/self/status`lo")
Expect(result).To(Equal("$'\\'h\\'el`cat /proc/self/status`lo'"))
})

It("should handle already escaped single quotes", func() {
result := EscapeBashStr("\\'h\\'el`cat /proc/self/status`lo")
Expect(result).To(Equal("$'\\\\\\'h\\\\\\'el`cat /proc/self/status`lo'"))
})

It("should handle ANSI-C quoted strings with single quotes", func() {
result := EscapeBashStr("$'h'el`cat /proc/self/status`lo")
Expect(result).To(Equal("$'$\\'h\\'el`cat /proc/self/status`lo'"))
})
})

Context("when string contains backslashes", func() {
It("should escape backslash before backtick", func() {
result := EscapeBashStr("hel\\`cat /proc/self/status`lo")
Expect(result).To(Equal("$'hel\\\\`cat /proc/self/status`lo'"))
})

It("should handle double backslash before backtick", func() {
result := EscapeBashStr("hel\\\\`cat /proc/self/status`lo")
Expect(result).To(Equal("$'hel\\\\\\\\`cat /proc/self/status`lo'"))
})

It("should handle backslash with single quote and backtick", func() {
result := EscapeBashStr("hel\\'`cat /proc/self/status`lo")
Expect(result).To(Equal("$'hel\\\\\\'`cat /proc/self/status`lo'"))
})

It("should handle multiple backslashes", func() {
result := EscapeBashStr("test\\\\\\\\value")
Expect(result).To(Equal("$'test\\\\\\\\\\\\\\\\value'"))
})
Comment thread
yuvraj-kolkar17 marked this conversation as resolved.
})

Context("when string contains shell operators", func() {
It("should escape ampersand", func() {
result := EscapeBashStr("command1 & command2")
Expect(result).To(Equal("$'command1 & command2'"))
})

It("should escape semicolon", func() {
result := EscapeBashStr("command1; command2")
Expect(result).To(Equal("$'command1; command2'"))
})

It("should escape pipe", func() {
result := EscapeBashStr("command1 | command2")
Expect(result).To(Equal("$'command1 | command2'"))
})

It("should escape greater than", func() {
result := EscapeBashStr("echo test > file.txt")
Expect(result).To(Equal("$'echo test > file.txt'"))
})

It("should escape parentheses", func() {
result := EscapeBashStr("(command1)")
Expect(result).To(Equal("$'(command1)'"))
})
})

Context("when string contains multiple special characters", func() {
It("should handle complex injection attempts", func() {
result := EscapeBashStr("'; rm -rf /; echo '")
Expect(result).To(Equal("$'\\'; rm -rf /; echo \\''"))
})

It("should handle dollar sign with ampersand", func() {
result := EscapeBashStr("$VAR && malicious")
Expect(result).To(Equal("$'$VAR && malicious'"))
})

It("should handle nested quotes and operators", func() {
result := EscapeBashStr("'test' | grep 'pattern' > output.txt")
Expect(result).To(Equal("$'\\'test\\' | grep \\'pattern\\' > output.txt'"))
})
})

Context("edge cases", func() {
It("should handle empty string", func() {
result := EscapeBashStr("")
Expect(result).To(Equal(""))
})

It("should handle string with only spaces", func() {
result := EscapeBashStr(" ")
Expect(result).To(Equal("$' '"))
})

It("should handle string with newlines (security critical)", func() {
result := EscapeBashStr("line1\nline2")
Expect(result).To(Equal("$'line1\\nline2'"))
})

It("should handle string starting with special character", func() {
result := EscapeBashStr("$test")
Expect(result).To(Equal("$'$test'"))
})

It("should handle string ending with special character", func() {
result := EscapeBashStr("test$")
Expect(result).To(Equal("$'test$'"))
})

It("should handle newline character injection attempt", func() {
result := EscapeBashStr("line1\nrm -rf /")
Expect(result).To(Equal("$'line1\\nrm -rf /'"))
})

It("should handle carriage return injection", func() {
result := EscapeBashStr("test\rmalicious")
Expect(result).To(Equal("$'test\\rmalicious'"))
})

It("should handle tab character", func() {
result := EscapeBashStr("test\tvalue")
Expect(result).To(Equal("$'test\\tvalue'"))
})
})
})

var _ = Describe("containsOne", func() {
Context("when checking for character existence", func() {
It("should return true when target contains one of the characters", func() {
backtick := '`'
result := containsOne("hello$world", []rune{'$', backtick, '&'})
Expect(result).To(BeTrue())
})

It("should return true when target contains multiple matching characters", func() {
result := containsOne("$test&value", []rune{'$', '&', ';'})
Expect(result).To(BeTrue())
})

It("should return false when target contains none of the characters", func() {
backtick := '`'
result := containsOne("hello-world", []rune{'$', backtick, '&'})
Expect(result).To(BeFalse())
})

It("should return false for empty string", func() {
backtick := '`'
result := containsOne("", []rune{'$', backtick, '&'})
Expect(result).To(BeFalse())
})

It("should return false when checking empty character list", func() {
result := containsOne("hello$world", []rune{})
Expect(result).To(BeFalse())
})

It("should handle single character target", func() {
backtick := '`'
result := containsOne("$", []rune{'$', backtick})
Expect(result).To(BeTrue())
})

It("should handle unicode characters", func() {
result := containsOne("hello世界", []rune{'世', '$'})
Expect(result).To(BeTrue())
})

It("should be case sensitive", func() {
result := containsOne("Hello", []rune{'h'})
Expect(result).To(BeFalse())
})

It("should detect backslash character", func() {
result := containsOne("test\\value", []rune{'\\', '$'})
Expect(result).To(BeTrue())
})

It("should detect single quote character", func() {
result := containsOne("test'value", []rune{'\'', '$'})
Expect(result).To(BeTrue())
})

It("should detect newline character", func() {
result := containsOne("test\nvalue", []rune{'\n', '$'})
Expect(result).To(BeTrue())
})
})
})
Loading