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
+35 -6
View File
@@ -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()
+38
View File
@@ -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 := `[