Merge pull request #5405 from SABITHSAHEB/wsl2-ssh-address-validate

wsl2: validate guest-reported address in getSSHAddress
This commit is contained in:
Jan Dubois authored and GitHub committed 2026-10-02 15:41:30 -07:00
commit d0367d0d3b
3 files changed
+73 -8

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
package wsl2
import (
"net"
"strings"
)
// routableGuestIP returns the trimmed output when it is a global unicast IP
// address (private and ULA ranges included), or "" otherwise. The output comes
// from a command run inside the WSL2 guest and becomes inst.SSHAddress, which
// is passed to ssh/scp/rsync as the destination argument, so anything that is
// not an address we can connect to is rejected here and the caller falls
// through to the next probe. Besides non-address output this rejects the
// unspecified, loopback (some distributions report 127.0.1.1), link-local and
// multicast addresses.
func routableGuestIP(out []byte) string {
s := strings.TrimSpace(string(out))
if ip := net.ParseIP(s); ip == nil || !ip.IsGlobalUnicast() {
return ""
}
return s
}
+39
View File
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
package wsl2
import (
"testing"
"gotest.tools/v3/assert"
)
func TestRoutableGuestIP(t *testing.T) {
tests := []struct {
name string
out string
want string
}{
{"ipv4", "192.168.5.10\n", "192.168.5.10"},
{"ipv4 trailing space", "10.0.0.5 \n", "10.0.0.5"},
{"ipv6", "fd00::1\n", "fd00::1"},
{"loopback rejected", "127.0.1.1\n", ""},
{"ipv6 loopback rejected", "::1", ""},
{"unspecified rejected", "0.0.0.0\n", ""},
{"ipv6 unspecified rejected", "::", ""},
{"link-local rejected", "169.254.1.1\n", ""},
{"ipv6 link-local rejected", "fe80::1", ""},
{"multicast rejected", "224.0.0.1\n", ""},
{"broadcast rejected", "255.255.255.255", ""},
{"empty rejected", "", ""},
{"ssh option injection rejected", "-oProxyCommand=calc.exe\n", ""},
{"ssh flag injection rejected", "-J attacker.example:22", ""},
{"hostname rejected", "attacker.example.com", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, routableGuestIP([]byte(tt.out)), tt.want)
})
}
}
+9 -8
View File
@@ -7,7 +7,6 @@ import (
"context"
_ "embed"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
@@ -269,23 +268,25 @@ func getSSHAddress(ctx context.Context, instName string) (string, error) {
cmd := exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`)
out, err := cmd.CombinedOutput()
if err == nil {
return strings.TrimSpace(string(out)), nil
if addr := routableGuestIP(out); addr != "" {
return addr, nil
}
}
// Alpine
cmd = exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "sh", "-c", `ip route get 1 | awk '{gsub("^.*src ",""); print $1; exit}'`)
out, err = cmd.CombinedOutput()
if err == nil {
return strings.TrimSpace(string(out)), nil
if addr := routableGuestIP(out); addr != "" {
return addr, nil
}
}
// fallback
cmd = exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "hostname", "-i")
out, err = cmd.CombinedOutput()
if err == nil {
ip := net.ParseIP(strings.TrimSpace(string(out)))
// some distributions use "127.0.1.1" as the host IP, but we want something that we can route to here
if ip != nil && !ip.IsLoopback() {
return strings.TrimSpace(string(out)), nil
if addr := routableGuestIP(out); addr != "" {
return addr, nil
}
}
return "", fmt.Errorf("failed to get hostname for instance %#q, err: %w (out=%#q)", instName, err, string(out))
return "", fmt.Errorf("failed to get a routable address for instance %#q, err: %w (out=%#q)", instName, err, string(out))
}