Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions ssh/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"net"
"os"
"strings"
"sync"
"time"
)
Expand Down Expand Up @@ -358,11 +359,32 @@ func FixedHostKey(key PublicKey) HostKeyCallback {
}

// BannerDisplayStderr returns a function that can be used for
// ClientConfig.BannerCallback to display banners on os.Stderr.
// ClientConfig.BannerCallback to display banners on os.Stderr. Control
// characters, other than tab, carriage return, and newline, are filtered
// out to prevent a malicious server from manipulating the client's
// terminal. To display a banner without filtering, implement a custom
// BannerCallback.
func BannerDisplayStderr() BannerCallback {
return func(banner string) error {
_, err := os.Stderr.WriteString(banner)
_, err := os.Stderr.WriteString(sanitizeBanner(banner))

return err
}
}

// sanitizeBanner strips control characters that could be used to
// manipulate the terminal, keeping tab, carriage return, newline, and
// printable text. Banners are UTF-8 encoded per RFC 4252, section 5.4,
// so non-ASCII text is preserved.
func sanitizeBanner(s string) string {
return strings.Map(func(r rune) rune {
if r == '\t' || r == '\r' || r == '\n' {
return r
}
// Strip the C0 and C1 control ranges as well as DEL.
if r < ' ' || (r >= 0x7f && r < 0xa0) {
return -1
}
return r
}, s)
}
49 changes: 49 additions & 0 deletions ssh/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"crypto/rand"
"errors"
"fmt"
"io"
"net"
"os"
"strings"
"testing"
)
Expand Down Expand Up @@ -209,6 +211,53 @@ func TestBannerCallback(t *testing.T) {
}
}

func TestSanitizeBanner(t *testing.T) {
for _, tc := range []struct {
in string
want string
}{
{"clean banner", "clean banner"},
{"line1\r\nline2\nline3", "line1\r\nline2\nline3"},
{"tab\there", "tab\there"},
{"\x1b[31mred\x1b[0m", "[31mred[0m"},
{"\x1b]0;evil\x07title", "]0;eviltitle"},
{"has\x00null\x00bytes", "hasnullbytes"},
{"del\x7fchar", "delchar"},
{"\u009b31mcsi", "31mcsi"},
{"héllo — 你好", "héllo — 你好"},
} {
t.Run(tc.in, func(t *testing.T) {
if got := sanitizeBanner(tc.in); got != tc.want {
t.Errorf("got %q, want %q", got, tc.want)
}
})
}
}

func TestBannerDisplayStderr(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
}
defer r.Close()
origStderr := os.Stderr
os.Stderr = w
defer func() { os.Stderr = origStderr }()
err = BannerDisplayStderr()("\x1b]0;evil\x07banner \x1b[31mtext\x1b[0m\r\n")
os.Stderr = origStderr
w.Close()
if err != nil {
t.Fatalf("BannerDisplayStderr: %v", err)
}
got, err := io.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if want := "]0;evilbanner [31mtext[0m\r\n"; string(got) != want {
t.Errorf("got %q, want %q", got, want)
}
}

func TestNewClientConn(t *testing.T) {
errHostKeyMismatch := errors.New("host key mismatch")

Expand Down