fix: harden local proxy and import boundaries

This commit is contained in:
Andy Dixon
2026-06-16 17:09:20 +01:00
parent 739651d548
commit cb2fbf70eb
12 changed files with 1189 additions and 49 deletions
+48 -20
View File
@@ -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 != "" {
+46
View File
@@ -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{
{