86 lines
4.4 KiB
Go
86 lines
4.4 KiB
Go
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)
|
||
}
|