mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2026-08-28 02:34:14 -05:00
fix(terminal): strip control runes from rendered segment content
Segment text (directory names, git commit metadata, environment variables, command output) can be attacker-controlled and reached the terminal unfiltered through write(s rune), the only sink Oh My Posh's own styling never uses. Raw ESC/BEL/CSI/OSC bytes let an attacker inject terminal escape sequences (title spoofing, OSC 52 clipboard writes) via a malicious directory name or git commit subject. GHSA-fwjx-9p69-h25h Entire-Checkpoint: 65f209a5fba9
This commit is contained in:
committed by
Jan De Dobbeleer
parent
88ddbe0b0a
commit
edcf3c88f3
@@ -674,6 +674,15 @@ func write(s rune) {
|
||||
return
|
||||
}
|
||||
|
||||
// segment content (directory names, git metadata, environment variables,
|
||||
// command output) is potentially attacker-controlled and never passes through
|
||||
// this function when it's Oh My Posh's own styling; drop C0/C1 control runes
|
||||
// (ESC, BEL, CSI, OSC, ...) so they can't be interpreted as escape sequences
|
||||
// by the terminal, including inside a hyperlink target.
|
||||
if isControlRune(s) {
|
||||
return
|
||||
}
|
||||
|
||||
if isHyperlink {
|
||||
builder.WriteRune(s)
|
||||
return
|
||||
@@ -694,6 +703,21 @@ func write(s rune) {
|
||||
builder.WriteRune(s)
|
||||
}
|
||||
|
||||
// isControlRune reports whether s is a C0 (0x00-0x1F), DEL (0x7F), or C1
|
||||
// (0x80-0x9F) control character. These are the bytes a terminal can interpret
|
||||
// as the start of an escape sequence (ESC, BEL, CSI, OSC, ...); no legitimate
|
||||
// rendered segment content needs them. '\n' is exempt: Oh My Posh itself
|
||||
// prepends a literal newline ahead of the transient prompt (see
|
||||
// Engine.getNewline), and unlike ESC/BEL/CSI/OSC a bare LF can't be
|
||||
// interpreted as the start of an escape sequence.
|
||||
func isControlRune(s rune) bool {
|
||||
if s == '\n' {
|
||||
return false
|
||||
}
|
||||
|
||||
return s <= 0x1f || (s >= 0x7f && s <= 0x9f)
|
||||
}
|
||||
|
||||
// writeVisibleRune stamps the active gradient color(s) for the current cell
|
||||
// before writing s, then advances cellIndex by s's rune width. It is only
|
||||
// called from writeBodyGradient, so isInvisible/isHyperlink runes are
|
||||
|
||||
@@ -93,6 +93,11 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
|
||||
ShellName: shell.BASH,
|
||||
Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\]\\[\x1b]8;;http://www.google.be\x1b\\\\\\]link\\[\x1b]8;;\x1b\\\\\\]\\[\x1b[0m\\]",
|
||||
},
|
||||
{
|
||||
Text: "<LINK>http://evil\x1b]0;HACKED\x07.com<TEXT>google</TEXT></LINK>",
|
||||
ShellName: shell.FISH,
|
||||
Expected: "\x1b[47m\x1b[30m\x1b]8;;http://evil]0;HACKED.com\x1b\\google\x1b]8;;\x1b\\\x1b[0m",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
Init(tc.ShellName)
|
||||
|
||||
@@ -219,6 +219,18 @@ func TestWriteANSIColors(t *testing.T) {
|
||||
Expected: "\x1b[33mhello \x1b[48;2;130;170;255m\x1b[38;2;1;22;39mnew\x1b[49m\x1b[33m world\x1b[0m",
|
||||
Colors: &color.Set{Foreground: "yellow", Background: "transparent"},
|
||||
},
|
||||
{
|
||||
Case: "OSC injection stripped from segment content",
|
||||
Input: "before\x1b]0;HACKED\x07after",
|
||||
Expected: "\x1b[47m\x1b[30mbefore]0;HACKEDafter\x1b[0m",
|
||||
Colors: &color.Set{Foreground: "black", Background: "white"},
|
||||
},
|
||||
{
|
||||
Case: "CSI and C1 control runes stripped from segment content",
|
||||
Input: "before\x1b[31mred\u009bafter",
|
||||
Expected: "\x1b[47m\x1b[30mbefore[31mredafter\x1b[0m",
|
||||
Colors: &color.Set{Foreground: "black", Background: "white"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
Reference in New Issue
Block a user