github.com/StackExchange/blackbox/v2@v2.0.1-0.20220331193400-d84e904973ab/pkg/makesafe/makesafe.go (about) 1 package makesafe 2 3 // untaint -- A string with a Stringer that is shell safe. 4 5 // This goes to great lengths to make sure the String() is pastable. 6 // Whitespace and shell "special chars" are handled as expected. 7 8 // However to be extra paranoid, unicode is turned into backtick 9 // printf statements. I don't know anyone that puts unicode in their 10 // filenames, but I hope they appreciate this. 11 12 // Most people would just use strconv.QuoteToGraphic() but I'm a 13 // control freak. 14 15 import ( 16 "fmt" 17 "strings" 18 "unicode" 19 ) 20 21 type protection int 22 23 const ( 24 // Unknown indicates we don't know if it is safe. 25 Unknown protection = iota 26 // None requires no special escaping. 27 None // Nothing special 28 // SingleQuote is unsafe in bash and requires a single quote. 29 SingleQuote // Requires at least a single quote 30 // DoubleQuote is unsafe in bash and requires escaping or other double-quote features. 31 DoubleQuote // Can only be in a double-quoted string 32 ) 33 34 const ( 35 // IsAQuote is either a `'` or `"` 36 IsAQuote = None 37 // IsSpace is ascii 32 38 IsSpace = SingleQuote 39 // ShellUnsafe is ()!$ or other bash special char 40 ShellUnsafe = SingleQuote 41 // GlobUnsafe means could be a glob char (* or ?) 42 GlobUnsafe = SingleQuote 43 // InterpolationUnsafe used in bash string interpolation ($) 44 InterpolationUnsafe = SingleQuote 45 // HasBackslash things like \n \t \r \000 \xFF 46 HasBackslash = DoubleQuote 47 ) 48 49 func max(i, j protection) protection { 50 if i > j { 51 return i 52 } 53 return j 54 55 } 56 57 type tabEntry struct { 58 level protection 59 fn func(s rune) string 60 } 61 62 var tab [128]tabEntry 63 64 func init() { 65 66 for i := 0; i <= 31; i++ { // Control chars 67 tab[i] = tabEntry{HasBackslash, oct()} 68 } 69 tab['\t'] = tabEntry{HasBackslash, literal(`\t`)} // Override 70 tab['\n'] = tabEntry{HasBackslash, literal(`\n`)} // Override 71 tab['\r'] = tabEntry{HasBackslash, literal(`\r`)} // Override 72 tab[' '] = tabEntry{IsSpace, same()} 73 tab['!'] = tabEntry{ShellUnsafe, same()} 74 tab['"'] = tabEntry{IsAQuote, same()} 75 tab['#'] = tabEntry{ShellUnsafe, same()} 76 tab['@'] = tabEntry{InterpolationUnsafe, same()} 77 tab['$'] = tabEntry{InterpolationUnsafe, same()} 78 tab['%'] = tabEntry{InterpolationUnsafe, same()} 79 tab['&'] = tabEntry{ShellUnsafe, same()} 80 tab['\''] = tabEntry{IsAQuote, same()} 81 tab['('] = tabEntry{ShellUnsafe, same()} 82 tab[')'] = tabEntry{ShellUnsafe, same()} 83 tab['*'] = tabEntry{GlobUnsafe, same()} 84 tab['+'] = tabEntry{GlobUnsafe, same()} 85 tab[','] = tabEntry{None, same()} 86 tab['-'] = tabEntry{None, same()} 87 tab['.'] = tabEntry{None, same()} 88 tab['/'] = tabEntry{None, same()} 89 for i := '0'; i <= '9'; i++ { 90 tab[i] = tabEntry{None, same()} 91 } 92 tab[':'] = tabEntry{InterpolationUnsafe, same()} // ${foo:=default} 93 tab[';'] = tabEntry{ShellUnsafe, same()} 94 tab['<'] = tabEntry{ShellUnsafe, same()} 95 tab['='] = tabEntry{InterpolationUnsafe, same()} // ${foo:=default} 96 tab['>'] = tabEntry{ShellUnsafe, same()} 97 tab['?'] = tabEntry{GlobUnsafe, same()} 98 tab['@'] = tabEntry{InterpolationUnsafe, same()} // ${myarray[@]}; 99 for i := 'A'; i <= 'Z'; i++ { 100 tab[i] = tabEntry{None, same()} 101 } 102 tab['['] = tabEntry{ShellUnsafe, same()} 103 tab['\\'] = tabEntry{ShellUnsafe, same()} 104 tab[']'] = tabEntry{GlobUnsafe, same()} 105 tab['^'] = tabEntry{GlobUnsafe, same()} 106 tab['_'] = tabEntry{None, same()} 107 tab['`'] = tabEntry{ShellUnsafe, same()} 108 for i := 'a'; i <= 'z'; i++ { 109 tab[i] = tabEntry{None, same()} 110 } 111 tab['{'] = tabEntry{ShellUnsafe, same()} 112 tab['|'] = tabEntry{ShellUnsafe, same()} 113 tab['}'] = tabEntry{ShellUnsafe, same()} 114 tab['~'] = tabEntry{ShellUnsafe, same()} 115 tab[127] = tabEntry{HasBackslash, oct()} 116 117 // Check our work. All indexes should have been set. 118 for i, e := range tab { 119 if e.level == 0 || e.fn == nil { 120 panic(fmt.Sprintf("tabEntry %d not set!", i)) 121 } 122 } 123 124 } 125 126 // literal return this exact string. 127 func literal(s string) func(s rune) string { 128 return func(rune) string { return s } 129 } 130 131 // same converts the rune to a string. 132 func same() func(r rune) string { 133 return func(r rune) string { return string(r) } 134 } 135 136 // oct returns the octal representing the value. 137 func oct() func(r rune) string { 138 return func(r rune) string { return fmt.Sprintf(`\%03o`, r) } 139 } 140 141 // Redact returns a string that can be used in a shell single-quoted 142 // string. It may not be an exact representation, but it is safe 143 // to include on a command line. 144 // 145 // Redacted chars are changed to "X". 146 // If anything is redacted, the string is surrounded by double quotes 147 // ("air quotes") and the string "(redacted)" is added to the end. 148 // If nothing is redacted, but it contains spaces, it is surrounded 149 // by double quotes. 150 // 151 // Example: `s` -> `s` 152 // Example: `space cadet.txt` -> `"space cadet.txt"` 153 // Example: `drink a \t soda` -> `"drink a X soda"(redacted)` 154 // Example: `smile☺` -> `"smile☺` 155 func Redact(tainted string) string { 156 157 if tainted == "" { 158 return `""` 159 } 160 161 var b strings.Builder 162 b.Grow(len(tainted) + 10) 163 164 redacted := false 165 needsQuote := false 166 167 for _, r := range tainted { 168 if r == ' ' { 169 b.WriteRune(r) 170 needsQuote = true 171 } else if r == '\'' { 172 b.WriteRune('X') 173 redacted = true 174 } else if r == '"' { 175 b.WriteRune('\\') 176 b.WriteRune(r) 177 needsQuote = true 178 } else if unicode.IsPrint(r) { 179 b.WriteRune(r) 180 } else { 181 b.WriteRune('X') 182 redacted = true 183 } 184 } 185 186 if redacted { 187 return `"` + b.String() + `"(redacted)` 188 } 189 if needsQuote { 190 return `"` + b.String() + `"` 191 } 192 return tainted 193 } 194 195 // RedactMany returns the list after processing each element with Redact(). 196 func RedactMany(items []string) []string { 197 var r []string 198 for _, n := range items { 199 r = append(r, Redact(n)) 200 } 201 return r 202 } 203 204 // Shell returns the string formatted so that it is safe to be pasted 205 // into a command line to produce the desired filename as an argument 206 // to the command. 207 func Shell(tainted string) string { 208 if tainted == "" { 209 return `""` 210 } 211 212 var b strings.Builder 213 b.Grow(len(tainted) + 10) 214 215 level := Unknown 216 for _, r := range tainted { 217 if r < 128 { 218 level = max(level, tab[r].level) 219 b.WriteString(tab[r].fn(r)) 220 } else { 221 level = max(level, DoubleQuote) 222 b.WriteString(escapeRune(r)) 223 } 224 } 225 s := b.String() 226 227 if level == None { 228 return tainted 229 } else if level == SingleQuote { 230 // A single quoted string accepts all chars except the single 231 // quote itself, which must be replaced with: '"'"' 232 return "'" + strings.Join(strings.Split(s, "'"), `'"'"'`) + "'" 233 } else if level == DoubleQuote { 234 // A double-quoted string may include \xxx escapes and other 235 // things. Sadly bash doesn't interpret those, but printf will! 236 return `$(printf '%q' '` + s + `')` 237 } 238 // should not happen 239 return fmt.Sprintf("%q", s) 240 } 241 242 // escapeRune returns a string of octal escapes that represent the rune. 243 func escapeRune(r rune) string { 244 b := []byte(string(rune(r))) // Convert to the indivdual bytes, utf8-encoded. 245 // fmt.Printf("rune: len=%d %s %v\n", len(s), s, []byte(s)) 246 switch len(b) { 247 case 1: 248 return fmt.Sprintf(`\%03o`, b[0]) 249 case 2: 250 return fmt.Sprintf(`\%03o\%03o`, b[0], b[1]) 251 case 3: 252 return fmt.Sprintf(`\%03o\%03o\%03o`, b[0], b[1], b[2]) 253 case 4: 254 return fmt.Sprintf(`\%03o\%03o\%03o\%03o`, b[0], b[1], b[2], b[3]) 255 default: 256 return string(rune(r)) 257 } 258 } 259 260 // ShellMany returns the list after processing each element with Shell(). 261 func ShellMany(items []string) []string { 262 var r []string 263 for _, n := range items { 264 r = append(r, Redact(n)) 265 } 266 return r 267 } 268 269 // FirstFew returns the first few names. If any are truncated, it is 270 // noted by appending "...". The exact definition of "few" may change 271 // over time, and may be based on the number of chars not the list 272 func FirstFew(sl []string) string { 273 s, _ := FirstFewFlag(sl) 274 return s 275 } 276 277 // FirstFewFlag is like FirstFew but returns true if truncation done. 278 func FirstFewFlag(sl []string) (string, bool) { 279 const maxitems = 2 280 const maxlen = 70 281 if len(sl) < maxitems || len(strings.Join(sl, " ")) < maxlen { 282 return strings.Join(sl, " "), false 283 } 284 return strings.Join(sl[:maxitems], " ") + " (and others)", true 285 }