diff options
author | zeripath <art27@cantab.net> | 2020-09-29 02:16:52 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-28 21:16:52 -0400 |
commit | 4c6ac08182b5a14eaaffaafafef160bd90c4ae81 (patch) | |
tree | 500fcc347d4ae741c4e0950beddb199e59ad6d6c /modules/util/shellquote.go | |
parent | 5cfc1f573fd986cf141165d52dd18255069f0d06 (diff) | |
download | gitea-4c6ac08182b5a14eaaffaafafef160bd90c4ae81.tar.gz gitea-4c6ac08182b5a14eaaffaafafef160bd90c4ae81.zip |
Completely quote AppPath and CustomConf paths (#12955)
* Completely quote AppPath and CustomConf paths
Properly handle spaces in AppPath and CustomConf within hooks and
authorized_keys. Unfortunately here we don't seem to be able to get away
with using go-shellquote as it appears that Windows doesn't play too
well with singlequote quoting - therefore we will avoid singlequote
quoting unless we absolutely cannot get away without it, e.g. \n or !.
Fix #10813
Signed-off-by: Andrew Thornton <art27@cantab.net>
* missing change
Signed-off-by: Andrew Thornton <art27@cantab.net>
* fix Test_CmdKeys
Signed-off-by: Andrew Thornton <art27@cantab.net>
Diffstat (limited to 'modules/util/shellquote.go')
-rw-r--r-- | modules/util/shellquote.go | 100 |
1 files changed, 100 insertions, 0 deletions
diff --git a/modules/util/shellquote.go b/modules/util/shellquote.go new file mode 100644 index 0000000000..bde24a1517 --- /dev/null +++ b/modules/util/shellquote.go @@ -0,0 +1,100 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package util + +import "strings" + +// Bash has the definition of a metacharacter: +// * A character that, when unquoted, separates words. +// A metacharacter is one of: " \t\n|&;()<>" +// +// The following characters also have addition special meaning when unescaped: +// * ‘${[*?!"'`\’ +// +// Double Quotes preserve the literal value of all characters with then quotes +// excepting: ‘$’, ‘`’, ‘\’, and, when history expansion is enabled, ‘!’. +// The backslash retains its special meaning only when followed by one of the +// following characters: ‘$’, ‘`’, ‘"’, ‘\’, or newline. +// Backslashes preceding characters without a special meaning are left +// unmodified. A double quote may be quoted within double quotes by preceding +// it with a backslash. If enabled, history expansion will be performed unless +// an ‘!’ appearing in double quotes is escaped using a backslash. The +// backslash preceding the ‘!’ is not removed. +// +// -> This means that `!\n` cannot be safely expressed in `"`. +// +// Looking at the man page for Dash and ash the situation is similar. +// +// Now zsh requires that ‘}’, and ‘]’ are also enclosed in doublequotes or escaped +// +// Single quotes escape everything except a ‘'’ +// +// There's one other gotcha - ‘~’ at the start of a string needs to be expanded +// because people always expect that - of course if there is a special character before '/' +// this is not going to work + +const ( + tildePrefix = '~' + needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\" + needsSingleQuote = "!\n" +) + +var doubleQuoteEscaper = strings.NewReplacer(`$`, `\$`, "`", "\\`", `"`, `\"`, `\`, `\\`) +var singleQuoteEscaper = strings.NewReplacer(`'`, `'\''`) +var singleQuoteCoalescer = strings.NewReplacer(`''\'`, `\'`, `\'''`, `\'`) + +// ShellEscape will escape the provided string. +// We can't just use go-shellquote here because our preferences for escaping differ from those in that we want: +// +// * If the string doesn't require any escaping just leave it as it is. +// * If the string requires any escaping prefer double quote escaping +// * If we have ! or newlines then we need to use single quote escaping +func ShellEscape(toEscape string) string { + if len(toEscape) == 0 { + return toEscape + } + + start := 0 + + if toEscape[0] == tildePrefix { + // We're in the forcibly non-escaped section... + idx := strings.IndexRune(toEscape, '/') + if idx < 0 { + idx = len(toEscape) + } else { + idx++ + } + if !strings.ContainsAny(toEscape[:idx], needsEscape) { + // We'll assume that they intend ~ expansion to occur + start = idx + } + } + + // Now for simplicity we'll look at the rest of the string + if !strings.ContainsAny(toEscape[start:], needsEscape) { + return toEscape + } + + // OK we have to do some escaping + sb := &strings.Builder{} + _, _ = sb.WriteString(toEscape[:start]) + + // Do we have any characters which absolutely need to be within single quotes - that is simply ! or \n? + if strings.ContainsAny(toEscape[start:], needsSingleQuote) { + // We need to single quote escape. + sb2 := &strings.Builder{} + _, _ = sb2.WriteRune('\'') + _, _ = singleQuoteEscaper.WriteString(sb2, toEscape[start:]) + _, _ = sb2.WriteRune('\'') + _, _ = singleQuoteCoalescer.WriteString(sb, sb2.String()) + return sb.String() + } + + // OK we can just use " just escape the things that need escaping + _, _ = sb.WriteRune('"') + _, _ = doubleQuoteEscaper.WriteString(sb, toEscape[start:]) + _, _ = sb.WriteRune('"') + return sb.String() +} |