CVE-2023-43809 (published 2023-10-04T21:15Z)

Soft Serve is a self-hostable Git server for the command line. Prior to version 0.6.2, a security vulnerability in Soft Serve could allow an unauthenticated, remote attacker to bypass public key authentication when keyboard-interactive SSH authentication is active, through the `allow-keyless` setting, and the public key requires additional client-side verification for example using FIDO2 or GPG. This is due to insufficient validation procedures of the public key step during SSH request handshake, granting unauthorized access if the keyboard-interaction mode is utilized. An attacker could exploit this vulnerability by presenting manipulated SSH requests using keyboard-interactive authentication mode. This could potentially result in unauthorized access to the Soft Serve. Users should upgrade to the latest Soft Serve version `v0.6.2` to receive the patch for this issue. To workaround this vulnerability without upgrading, users can temporarily disable Keyboard-Interactive SSH Authentication using the `allow-keyless` setting.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-287
cwe_name: Improper Authentication
description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
extended_description: 
            
			
         
         

Fix information:
repo_url: https://github.com/charmbracelet/soft-serve
commit_details: repo_url: https://github.com/charmbracelet/soft-serve
author: Ayman Bagabas
author_date: 2023-09-26 16:32:03-04:00
msg: fix(ssh): add authentication middleware

We need to verify that the key used to establish the connection is the
same key used for authentication, otherwise, refuse connection.
repository: repo_url: https://github.com/charmbracelet/soft-serve
repo_name: charmbracelet/soft-serve
description: The mighty, self-hostable Git server for the command line🍦
repo_language: Go
owner: charmbracelet
file_changes: filename: middleware.go
change_type: ModificationType.MODIFY
diff: @@ -13,11 +13,45 @@ import (
 	"github.com/charmbracelet/soft-serve/server/sshutils"
 	"github.com/charmbracelet/soft-serve/server/store"
 	"github.com/charmbracelet/ssh"
+	"github.com/charmbracelet/wish"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 	"github.com/spf13/cobra"
+	gossh "golang.org/x/crypto/ssh"
 )
 
+// ErrPermissionDenied is returned when a user is not allowed connect.
+var ErrPermissionDenied = fmt.Errorf("permission denied")
+
+// AuthenticationMiddleware handles authentication.
+func AuthenticationMiddleware(sh ssh.Handler) ssh.Handler {
+	return func(s ssh.Session) {
+		// XXX: The authentication key is set in the context but gossh doesn't
+		// validate the authentication. We need to verify that the _last_ key
+		// that was approved is the one that's being used.
+
+		pk := s.PublicKey()
+		if pk != nil {
+			// There is no public key stored in the context, public-key auth
+			// was never requested, skip
+			perms := s.Permissions().Permissions
+			if perms == nil {
+				wish.Fatalln(s, ErrPermissionDenied)
+				return
+			}
+
+			// Check if the key is the same as the one we have in context
+			fp := perms.Extensions["pubkey-fp"]
+			if fp != gossh.FingerprintSHA256(pk) {
+				wish.Fatalln(s, ErrPermissionDenied)
+				return
+			}
+		}
+
+		sh(s)
+	}
+}
+
 // ContextMiddleware adds the config, backend, and logger to the session context.
 func ContextMiddleware(cfg *config.Config, dbx *db.DB, datastore store.Store, be *backend.Backend, logger *log.Logger) func(ssh.Handler) ssh.Handler {
 	return func(sh ssh.Handler) ssh.Handler {

programming_language: Go
filename: ssh.go
change_type: ModificationType.MODIFY
diff: @@ -77,6 +77,11 @@ func NewSSHServer(ctx context.Context) (*SSHServer, error) {
 			LoggingMiddleware,
 			// Context middleware.
 			ContextMiddleware(cfg, dbx, datastore, be, logger),
+			// Authentication middleware.
+			// gossh.PublicKeyHandler doesn't guarantee that the public key
+			// is in fact the one used for authentication, so we need to
+			// check it again here.
+			AuthenticationMiddleware,
 		),
 	}
 
@@ -91,6 +96,16 @@ func NewSSHServer(ctx context.Context) (*SSHServer, error) {
 		return nil, err
 	}
 
+	if config.IsDebug() {
+		s.srv.ServerConfigCallback = func(ctx ssh.Context) *gossh.ServerConfig {
+			return &gossh.ServerConfig{
+				AuthLogCallback: func(conn gossh.ConnMetadata, method string, err error) {
+					logger.Debug("authentication", "user", conn.User(), "method", method, "err", err)
+				},
+			}
+		}
+	}
+
 	if cfg.SSH.MaxTimeout > 0 {
 		s.srv.MaxTimeout = time.Duration(cfg.SSH.MaxTimeout) * time.Second
 	}
@@ -130,6 +145,19 @@ func (s *SSHServer) Shutdown(ctx context.Context) error {
 	return s.srv.Shutdown(ctx)
 }
 
+func initializePermissions(ctx ssh.Context) {
+	perms := ctx.Permissions()
+	if perms == nil || perms.Permissions == nil {
+		perms = &ssh.Permissions{Permissions: &gossh.Permissions{}}
+	}
+	if perms.Extensions == nil {
+		perms.Extensions = make(map[string]string)
+	}
+	if perms.Permissions.Extensions == nil {
+		perms.Permissions.Extensions = make(map[string]string)
+	}
+}
+
 // PublicKeyAuthHandler handles public key authentication.
 func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed bool) {
 	if pk == nil {
@@ -144,6 +172,15 @@ func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed
 	if user != nil {
 		ctx.SetValue(proto.ContextKeyUser, user)
 		allowed = true
+
+		// XXX: store the first "approved" public-key fingerprint in the
+		// permissions block to use for authentication later.
+		initializePermissions(ctx)
+		perms := ctx.Permissions()
+
+		// Set the public key fingerprint to be used for authentication.
+		perms.Extensions["pubkey-fp"] = gossh.FingerprintSHA256(pk)
+		ctx.SetValue(ssh.ContextKeyPermissions, perms)
 	}
 
 	return
@@ -154,5 +191,16 @@ func (s *SSHServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) (allowed
 func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool {
 	ac := s.be.AllowKeyless(ctx)
 	keyboardInteractiveCounter.WithLabelValues(strconv.FormatBool(ac)).Inc()
+
+	// If we're allowing keyless access, reset the public key fingerprint
+	if ac {
+		initializePermissions(ctx)
+		perms := ctx.Permissions()
+
+		// XXX: reset the public-key fingerprint. This is used to validate the
+		// public key being used to authenticate.
+		perms.Extensions["pubkey-fp"] = ""
+		ctx.SetValue(ssh.ContextKeyPermissions, perms)
+	}
 	return ac
 }

programming_language: Go