Files
dingtalk-workspace-cli/internal/helpers/shell_quote.go

86 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helpers
import "strings"
// ShellQuoteArg 按 POSIX sh 规则把 s 引用成可安全放进可复制命令的单个 argv 元素。
//
// 为什么需要它:错误提示里的恢复命令是给用户直接复制到 shell 执行的,其中的查询域取自用户
// 输入(--workspace 的常见形态就是带查询串的 URL)。裸拼接下,合法 URL 里的 `&` 就会把命令
// 拆成后台任务,空格会拆参,`;` 与 `$()` 还能执行额外内容。
//
// 为什么不用 strconv.Quote(internal/auth 侧展示 profile 标识的既有做法):那是 Go 语法引号,
// 产出双引号串,而 shell 双引号内 `$()`、反引号、`$VAR` 仍会展开——正是要防的场景。auth 那处
// 成立是因为它显式声明「仅作数据展示,不是可执行命令」,本函数的产物恰恰要能执行。
//
// 策略是「必要时才引用」:全由安全字符组成时原样返回,命令保持可读、在 PowerShell/cmd 下同样
// 可复制;只要含一个非安全字符就整体单引号包裹——单引号内 POSIX sh 不做任何展开($ ` \ ! 全部
// 字面化),是唯一无需逐字符转义的形式。单引号自身无法出现在单引号串内,按 POSIX 标准写法
// 先闭合、拼一个反斜杠转义的单引号、再重开——即把每个单引号替换成下面这四个字符:
//
// '\''
//
// 空串必须显式引用成一对空单引号,否则该参数会从 argv 里整个消失,后面的 token 会被前一个
// flag 吞掉。
func ShellQuoteArg(s string) string {
if s == "" {
return "''"
}
if shellValueIsBare(s) {
return s
}
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}
// shellValueIsBare 报告 s 能否原样出现在命令行里而不改变**任何**常见 shell 的解析:非空,
// 且每个字符都落在保守白名单内。这类值在 POSIX sh、PowerShell 与 cmd.exe 下含义一致,
// 无需引用即可内联进可复制命令。
func shellValueIsBare(s string) bool {
return s != "" && !strings.ContainsFunc(s, shellNeedsQuote)
}
// driveLatestPosixScopeValue 是 POSIX shell 下把用户值放进可复制命令的策略:单引号内 sh 不做
// 任何展开,因此含元字符的值引用后即可安全内联。第二个返回值恒为 true。
func driveLatestPosixScopeValue(value string) (string, bool) {
return ShellQuoteArg(value), true
}
// driveLatestWindowsScopeValue 是 Windows 构建下的策略:只内联本身就安全的值,其余一律不进
// 命令,由调用方降级为占位符 + 展示行。
//
// Windows 上不存在「一条命令同时对 cmd.exe 与 PowerShell 都安全」的引用形式:
// - cmd.exe 根本不把单引号当引号,`--space-id 'sp-7&whoami'` 里的 & 仍然分隔命令,
// 粘贴即执行 whoami;
// - cmd.exe 的双引号能挡 & | < >,却挡不住 %VAR% 展开;
// - PowerShell 的单引号是引号,但内嵌单引号写作两个连续单引号,与 POSIX 的
// 闭合-反斜杠转义-重开写法不兼容。
//
// 而生成命令时无法知道用户会粘贴进哪个 shell。既然引用不可靠,就不把不受信任的值放进可执行
// 命令——与 internal/auth 侧「标识仅作数据展示,不是可执行命令」的既有做法同一思路。
func driveLatestWindowsScopeValue(value string) (string, bool) {
if shellValueIsBare(value) {
return value, true
}
return "", false
}
// shellNeedsQuote 报告 r 是否落在「无需引用」白名单之外。
//
// 用白名单而非黑名单:漏掉一个元字符就是一个注入口,而白名单漏一个字符只会多加一对无害的
// 引号。非 ASCII 一律视为需引用——中文目录名很常见,保守处理不会错。
//
// 白名单的门槛是「在 POSIX sh、PowerShell、cmd.exe 下含义都一致」,因此 % 被刻意排除:
// cmd.exe 会展开 %VAR%,一个看似普通的值(URL 里的 %20、含 %PATH% 的名字)原样内联后在
// cmd 里就会变形甚至泄露环境变量。@ 保留:PowerShell 的 splatting 只在 @( 、@{ 与
// @变量名 形态下生效,而那些字符本身都不在白名单里,且值总出现在 --flag 之后而非语句开头。
func shellNeedsQuote(r rune) bool {
switch {
case r >= 'a' && r <= 'z':
return false
case r >= 'A' && r <= 'Z':
return false
case r >= '0' && r <= '9':
return false
}
return !strings.ContainsRune("_@+=:,./-", r)
}