fix: harden local proxy and import boundaries
This commit is contained in:
+35
-6
@@ -49,6 +49,7 @@ const (
|
||||
maxSFTPUploadChunkBytes = 1024 * 1024
|
||||
maxStreamingSFTPUploadSize = int64(2 * 1024 * 1024 * 1024)
|
||||
maxSSHPublicKeyBytes = 64 * 1024
|
||||
sshKeygenTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// Service is the main API surface exposed to the frontend via Wails bindings.
|
||||
@@ -1809,6 +1810,10 @@ func (s *Service) GenerateSSHKey(name string, keyType string, bits int, passphra
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyType, bits, err = validateSSHKeyGenerationOptions(keyType, bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sshDir := filepath.Dir(keyPath)
|
||||
|
||||
// Ensure .ssh directory exists
|
||||
@@ -1821,10 +1826,6 @@ func (s *Service) GenerateSSHKey(name string, keyType string, bits int, passphra
|
||||
return fmt.Errorf("key %q already exists", name)
|
||||
}
|
||||
|
||||
if keyType == "" {
|
||||
keyType = "ed25519"
|
||||
}
|
||||
|
||||
// Build ssh-keygen arguments
|
||||
args := []string{"-t", keyType, "-f", keyPath, "-C", name + "@nexus-terminal"}
|
||||
if passphrase != "" {
|
||||
@@ -1837,10 +1838,15 @@ func (s *Service) GenerateSSHKey(name string, keyType string, bits int, passphra
|
||||
}
|
||||
|
||||
// #nosec G204 — arguments are sanitised above
|
||||
cmd := exec.Command("ssh-keygen", args...)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), sshKeygenTimeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "ssh-keygen", args...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return fmt.Errorf("ssh-keygen timed out after %s", sshKeygenTimeout)
|
||||
}
|
||||
return fmt.Errorf("ssh-keygen failed: %s", stderr.String())
|
||||
}
|
||||
|
||||
@@ -1929,12 +1935,35 @@ func safeSFTPUploadName(fileName string) (string, error) {
|
||||
}
|
||||
name = strings.ReplaceAll(name, "\\", "/")
|
||||
safeName := path.Base(name)
|
||||
if safeName == "" || safeName == "." || safeName == "/" || strings.ContainsRune(safeName, 0) {
|
||||
if safeName == "" || safeName == "." || safeName == ".." || safeName == "/" || strings.ContainsRune(safeName, 0) {
|
||||
return "", fmt.Errorf("invalid file name")
|
||||
}
|
||||
return safeName, nil
|
||||
}
|
||||
|
||||
func validateSSHKeyGenerationOptions(keyType string, bits int) (string, int, error) {
|
||||
keyType = strings.ToLower(strings.TrimSpace(keyType))
|
||||
if keyType == "" {
|
||||
keyType = "ed25519"
|
||||
}
|
||||
switch keyType {
|
||||
case "ed25519", "ecdsa":
|
||||
return keyType, bits, nil
|
||||
case "rsa":
|
||||
if bits == 0 {
|
||||
bits = 4096
|
||||
}
|
||||
switch bits {
|
||||
case 2048, 3072, 4096:
|
||||
return keyType, bits, nil
|
||||
default:
|
||||
return "", 0, fmt.Errorf("unsupported RSA key size %d (use 2048, 3072, or 4096)", bits)
|
||||
}
|
||||
default:
|
||||
return "", 0, fmt.Errorf("unsupported SSH key type %q", keyType)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) lookupSFTPUpload(uploadID string) (*sftpUpload, error) {
|
||||
s.uploadMu.Lock()
|
||||
defer s.uploadMu.Unlock()
|
||||
|
||||
@@ -138,6 +138,44 @@ func TestSafeSFTPUploadNamePreservesHTMLCharacters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSFTPUploadNameRejectsParentDirectory(t *testing.T) {
|
||||
for _, name := range []string{"..", "folder/..", `folder\..`} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := safeSFTPUploadName(name); err == nil {
|
||||
t.Fatal("expected parent directory upload name to be rejected")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSSHKeyGenerationOptions(t *testing.T) {
|
||||
keyType, bits, err := validateSSHKeyGenerationOptions("", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("default options error = %v", err)
|
||||
}
|
||||
if keyType != "ed25519" {
|
||||
t.Fatalf("default key type = %q, want ed25519", keyType)
|
||||
}
|
||||
if bits != 0 {
|
||||
t.Fatalf("default bits = %d, want 0", bits)
|
||||
}
|
||||
|
||||
keyType, bits, err = validateSSHKeyGenerationOptions("rsa", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("rsa default options error = %v", err)
|
||||
}
|
||||
if keyType != "rsa" || bits != 4096 {
|
||||
t.Fatalf("rsa defaults = %q/%d, want rsa/4096", keyType, bits)
|
||||
}
|
||||
|
||||
if _, _, err := validateSSHKeyGenerationOptions("rsa", 999999); err == nil {
|
||||
t.Fatal("expected oversized RSA key size to be rejected")
|
||||
}
|
||||
if _, _, err := validateSSHKeyGenerationOptions("dsa", 0); err == nil {
|
||||
t.Fatal("expected unsupported key type to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportProfilesNormalizesSparseRAWProfile(t *testing.T) {
|
||||
svc := newTestService(t)
|
||||
data := `[
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
const (
|
||||
maxGitHubJSONBodyBytes = 4 << 20
|
||||
maxGitHubErrorBodyBytes = 64 << 10
|
||||
maxGitHubArchiveBytes = int64(2 << 30)
|
||||
)
|
||||
|
||||
// Client wraps the GitHub REST API for repository listing.
|
||||
@@ -190,9 +192,16 @@ func (c *Client) fetchRepos(ctx context.Context, progress func(done int, total i
|
||||
|
||||
// FetchLatestCommit returns the committer name and commit time for a repo/ref.
|
||||
func (c *Client) FetchLatestCommit(ctx context.Context, fullName string, ref string) (string, string, error) {
|
||||
url := fmt.Sprintf("%s/repos/%s/commits/%s", c.baseURL, fullName, ref)
|
||||
if ref == "" {
|
||||
url = fmt.Sprintf("%s/repos/%s/commits/HEAD", c.baseURL, fullName)
|
||||
ref = "HEAD"
|
||||
}
|
||||
owner, repo, err := splitRepoFullName(fullName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
url, err := c.endpointURL("repos", owner, repo, "commits", ref)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
@@ -252,12 +261,17 @@ func (c *Client) FetchLatestCommit(ctx context.Context, fullName string, ref str
|
||||
|
||||
// DownloadRepoArchive downloads a repository zip archive to the given path.
|
||||
func (c *Client) DownloadRepoArchive(ctx context.Context, fullName string, ref string, destPath string) error {
|
||||
if fullName == "" {
|
||||
return fmt.Errorf("repository name cannot be empty")
|
||||
owner, repo, err := splitRepoFullName(fullName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("%s/repos/%s/zipball", c.baseURL, fullName)
|
||||
segments := []string{"repos", owner, repo, "zipball"}
|
||||
if ref != "" {
|
||||
url = url + "/" + ref
|
||||
segments = append(segments, ref)
|
||||
}
|
||||
url, err := c.endpointURL(segments...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
@@ -280,6 +294,9 @@ func (c *Client) DownloadRepoArchive(ctx context.Context, fullName string, ref s
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GitHub API returned HTTP %d: %s", resp.StatusCode, readGitHubErrorBody(resp.Body))
|
||||
}
|
||||
if resp.ContentLength > maxGitHubArchiveBytes {
|
||||
return fmt.Errorf("archive is too large: %d bytes exceeds %d bytes", resp.ContentLength, maxGitHubArchiveBytes)
|
||||
}
|
||||
|
||||
file, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
@@ -287,7 +304,7 @@ func (c *Client) DownloadRepoArchive(ctx context.Context, fullName string, ref s
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := io.Copy(file, resp.Body); err != nil {
|
||||
if _, err := copyLimitedArchive(file, resp.Body, maxGitHubArchiveBytes); err != nil {
|
||||
return fmt.Errorf("writing archive: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -345,3 +362,47 @@ func readGitHubErrorBody(r io.Reader) string {
|
||||
}
|
||||
return strings.TrimSpace(string(body))
|
||||
}
|
||||
|
||||
func splitRepoFullName(fullName string) (string, string, error) {
|
||||
parts := strings.Split(strings.TrimSpace(fullName), "/")
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("repository name must be in owner/repo format")
|
||||
}
|
||||
owner := strings.TrimSpace(parts[0])
|
||||
repo := strings.TrimSpace(parts[1])
|
||||
if owner == "" || repo == "" {
|
||||
return "", "", fmt.Errorf("repository name must be in owner/repo format")
|
||||
}
|
||||
return owner, repo, nil
|
||||
}
|
||||
|
||||
func (c *Client) endpointURL(segments ...string) (string, error) {
|
||||
u, err := url.Parse(c.baseURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid GitHub base URL: %w", err)
|
||||
}
|
||||
plainPath := strings.TrimRight(u.Path, "/")
|
||||
escapedPath := strings.TrimRight(u.EscapedPath(), "/")
|
||||
for _, segment := range segments {
|
||||
if segment == "" {
|
||||
return "", fmt.Errorf("empty GitHub API path segment")
|
||||
}
|
||||
plainPath += "/" + segment
|
||||
escapedPath += "/" + url.PathEscape(segment)
|
||||
}
|
||||
u.Path = plainPath
|
||||
u.RawPath = escapedPath
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func copyLimitedArchive(dst io.Writer, src io.Reader, maxBytes int64) (int64, error) {
|
||||
limited := &io.LimitedReader{R: src, N: maxBytes + 1}
|
||||
n, err := io.Copy(dst, limited)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if n > maxBytes {
|
||||
return n, fmt.Errorf("archive exceeds %d bytes", maxBytes)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package githubclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -24,3 +26,50 @@ func TestReadLimitedGitHubBodyAllowsResponseAtLimit(t *testing.T) {
|
||||
t.Fatalf("body = %q, want abc", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointURLEscapesRepoPathSegments(t *testing.T) {
|
||||
client := New("https://ghe.example.com/api/v3", "")
|
||||
|
||||
got, err := client.endpointURL("repos", "owner", "repo", "commits", "feature/a b")
|
||||
if err != nil {
|
||||
t.Fatalf("endpointURL() error = %v", err)
|
||||
}
|
||||
want := "https://ghe.example.com/api/v3/repos/owner/repo/commits/feature%2Fa%20b"
|
||||
if got != want {
|
||||
t.Fatalf("endpointURL() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitRepoFullNameRejectsExtraSegments(t *testing.T) {
|
||||
if _, _, err := splitRepoFullName("owner/repo/../../user"); err == nil {
|
||||
t.Fatal("expected malformed repository name to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyLimitedArchiveRejectsOversizedStream(t *testing.T) {
|
||||
var dst bytes.Buffer
|
||||
n, err := copyLimitedArchive(&dst, strings.NewReader("abcd"), 3)
|
||||
if err == nil {
|
||||
t.Fatal("expected oversized archive stream to be rejected")
|
||||
}
|
||||
if n != 4 {
|
||||
t.Fatalf("copied = %d, want 4", n)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "archive exceeds") {
|
||||
t.Fatalf("error = %q, want archive exceeds", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyLimitedArchiveAllowsStreamAtLimit(t *testing.T) {
|
||||
var dst bytes.Buffer
|
||||
n, err := copyLimitedArchive(&dst, strings.NewReader("abc"), 3)
|
||||
if err != nil {
|
||||
t.Fatalf("copyLimitedArchive() error = %v", err)
|
||||
}
|
||||
if n != 3 {
|
||||
t.Fatalf("copied = %d, want 3", n)
|
||||
}
|
||||
if got, err := io.ReadAll(&dst); err != nil || string(got) != "abc" {
|
||||
t.Fatalf("dst = %q, err = %v; want abc", string(got), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,22 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
maxConfigFileBytes = 5 * 1024 * 1024
|
||||
maxConfigLineBytes = 256 * 1024
|
||||
maxHostBlocks = 5000
|
||||
)
|
||||
|
||||
// HostBlock represents a parsed SSH config Host section.
|
||||
type HostBlock struct {
|
||||
Aliases []string
|
||||
HostName string
|
||||
Port int
|
||||
User string
|
||||
IdentityFile string
|
||||
ProxyJump string
|
||||
ForwardAgent bool
|
||||
Compression bool
|
||||
Aliases []string
|
||||
HostName string
|
||||
Port int
|
||||
User string
|
||||
IdentityFile string
|
||||
ProxyJump string
|
||||
ForwardAgent bool
|
||||
Compression bool
|
||||
}
|
||||
|
||||
// ParseFile reads and parses an OpenSSH config file.
|
||||
@@ -41,11 +47,33 @@ func ParseFile(path string) ([]HostBlock, error) {
|
||||
return nil, fmt.Errorf("sshconfig: failed to open %s: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sshconfig: failed to stat %s: %w", path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("sshconfig: %s is a directory", path)
|
||||
}
|
||||
if info.Size() > maxConfigFileBytes {
|
||||
return nil, fmt.Errorf("sshconfig: config file is too large: %d bytes exceeds %d bytes", info.Size(), maxConfigFileBytes)
|
||||
}
|
||||
|
||||
var blocks []HostBlock
|
||||
var current *HostBlock
|
||||
commitCurrent := func() error {
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
if len(blocks) >= maxHostBlocks {
|
||||
return fmt.Errorf("sshconfig: too many Host blocks (max %d)", maxHostBlocks)
|
||||
}
|
||||
blocks = append(blocks, *current)
|
||||
current = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), maxConfigLineBytes)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
@@ -61,8 +89,8 @@ func ParseFile(path string) ([]HostBlock, error) {
|
||||
switch key {
|
||||
case "host":
|
||||
// Commit previous block.
|
||||
if current != nil {
|
||||
blocks = append(blocks, *current)
|
||||
if err := commitCurrent(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aliases := strings.Fields(value)
|
||||
// Skip wildcard-only hosts (e.g. "Host *").
|
||||
@@ -83,8 +111,8 @@ func ParseFile(path string) ([]HostBlock, error) {
|
||||
|
||||
case "match":
|
||||
// Commit previous block and skip Match blocks.
|
||||
if current != nil {
|
||||
blocks = append(blocks, *current)
|
||||
if err := commitCurrent(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
current = nil
|
||||
|
||||
@@ -122,8 +150,8 @@ func ParseFile(path string) ([]HostBlock, error) {
|
||||
}
|
||||
|
||||
// Commit final block.
|
||||
if current != nil {
|
||||
blocks = append(blocks, *current)
|
||||
if err := commitCurrent(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
@@ -161,13 +189,13 @@ func ToProfiles(blocks []HostBlock, folder string) []models.Profile {
|
||||
username := b.User
|
||||
|
||||
sshConf := &models.SSHConfig{
|
||||
AuthMethods: []string{"publickey", "password"},
|
||||
SecretMode: models.SecretModeAskEveryTime,
|
||||
HostKeyPolicy: models.HostKeyAsk,
|
||||
TerminalType: "xterm-256color",
|
||||
AgentAuth: b.ForwardAgent,
|
||||
AuthMethods: []string{"publickey", "password"},
|
||||
SecretMode: models.SecretModeAskEveryTime,
|
||||
HostKeyPolicy: models.HostKeyAsk,
|
||||
TerminalType: "xterm-256color",
|
||||
AgentAuth: b.ForwardAgent,
|
||||
AgentForwarding: b.ForwardAgent,
|
||||
Compression: b.Compression,
|
||||
Compression: b.Compression,
|
||||
}
|
||||
|
||||
if b.IdentityFile != "" {
|
||||
|
||||
@@ -3,6 +3,8 @@ package sshconfig
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -89,6 +91,50 @@ func TestParseFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileRejectsOversizedFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config")
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Truncate(maxConfigFileBytes + 1); err != nil {
|
||||
f.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = ParseFile(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected oversized config file to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("error = %q, want too large", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileRejectsTooManyHostBlocks(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config")
|
||||
var b strings.Builder
|
||||
for i := 0; i < maxHostBlocks+1; i++ {
|
||||
b.WriteString("Host host")
|
||||
b.WriteString(strconv.Itoa(i))
|
||||
b.WriteString("\n HostName example.com\n")
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := ParseFile(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected excessive Host blocks to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "too many Host blocks") {
|
||||
t.Fatalf("error = %q, want too many Host blocks", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestToProfiles(t *testing.T) {
|
||||
blocks := []HostBlock{
|
||||
{
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -53,11 +55,13 @@ type Config struct {
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
CheckOrigin: allowProxyOrigin,
|
||||
ReadBufferSize: 65536,
|
||||
WriteBufferSize: 65536,
|
||||
}
|
||||
|
||||
const maxWebSocketMessageBytes = 8 << 20
|
||||
|
||||
// NewClient creates a new VNC proxy client.
|
||||
func NewClient(cfg Config) *Client {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -186,6 +190,7 @@ func (c *Client) startProxy(vncConn net.Conn) (string, error) {
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(maxWebSocketMessageBytes)
|
||||
|
||||
// Bridge WebSocket ↔ VNC TCP in both directions
|
||||
done := make(chan struct{}, 2)
|
||||
@@ -252,6 +257,34 @@ func (c *Client) startProxy(vncConn net.Conn) (string, error) {
|
||||
return wsURL, nil
|
||||
}
|
||||
|
||||
func allowProxyOrigin(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return true
|
||||
}
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "wails":
|
||||
return true
|
||||
case "http", "https":
|
||||
return isLoopbackHost(u.Hostname())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isLoopbackHost(host string) bool {
|
||||
h := strings.ToLower(strings.TrimSpace(host))
|
||||
if h == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(h)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func newProxyToken() (string, error) {
|
||||
token := make([]byte, 16)
|
||||
if _, err := rand.Read(token); err != nil {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package vncclient
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewProxyTokenIsRandomHex(t *testing.T) {
|
||||
first, err := newProxyToken()
|
||||
@@ -24,3 +27,35 @@ func TestNewProxyTokenIsRandomHex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowProxyOrigin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
origin string
|
||||
want bool
|
||||
}{
|
||||
{name: "missing origin", origin: "", want: true},
|
||||
{name: "wails origin", origin: "wails://wails", want: true},
|
||||
{name: "localhost http", origin: "http://localhost:34115", want: true},
|
||||
{name: "loopback ipv4", origin: "http://127.0.0.1:34115", want: true},
|
||||
{name: "loopback ipv6", origin: "http://[::1]:34115", want: true},
|
||||
{name: "remote http", origin: "http://example.com", want: false},
|
||||
{name: "lookalike host", origin: "http://127.0.0.1.example.com", want: false},
|
||||
{name: "bad origin", origin: "://bad", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1/ws", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.origin != "" {
|
||||
req.Header.Set("Origin", tt.origin)
|
||||
}
|
||||
if got := allowProxyOrigin(req); got != tt.want {
|
||||
t.Fatalf("allowProxyOrigin(%q) = %v, want %v", tt.origin, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user