Commit 13870b14 authored by Kevin Lyda's avatar Kevin Lyda
Browse files

Address several security issues

Now that registration can be open, did a security review and closed
a number of holes.
parent 4eafefee
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -217,13 +217,13 @@ Match User %s
    X11Forwarding no
    AllowTcpForwarding no
    PermitTunnel no
    ForceCommand %s --ssh
    ForceCommand /usr/sbin/nologin
    AuthorizedKeysCommand %s authorized-keys %%u %%t %%k
    AuthorizedKeysCommandUser %s

--- Then run: sudo systemctl reload sshd ---

`, os.Getenv("USER"), bulletin, bulletin, os.Getenv("USER"))
`, os.Getenv("USER"), bulletin, os.Getenv("USER"))
		_, _ = ask.GetLine("Press Enter to continue after configuring sshd...")
	}

+1 −1
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ import (
	"golang.org/x/sys/unix"
)

var keytemplate = `command="%s -u %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s`
var keytemplate = `command="%s --user %s",restrict %s`

// Add adds an ssh key to the `authorized_keys` file.
func Add(login, public string) error {
+22 −19
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import (
	"fmt"
	"os"
	"strings"
	"time"

	"git.lyda.ie/pp/bulletin/ask"
	"git.lyda.ie/pp/bulletin/authorized"
@@ -48,13 +49,16 @@ func Run(fingerprint, pubkeyStr string) int {
	if len(shibboleths) > 0 {
		fmt.Println("Before proceeding, please answer the following questions:")
		fmt.Println()
		for _, s := range shibboleths {
		for i, s := range shibboleths {
			answer, err := ask.GetLine(s.Question + " ")
			if err != nil {
				fmt.Println("Aborted.")
				return 1
			}
			if !strings.EqualFold(strings.TrimSpace(answer), strings.TrimSpace(s.Answer)) {
				// Rate limit: increasing delay on wrong answers.
				delay := time.Duration(i+1) * 2 * time.Second
				time.Sleep(delay)
				fmt.Println("Incorrect answer. Access denied.")
				return 1
			}
@@ -140,38 +144,37 @@ func createAccount(q *storage.Queries, fingerprint, pubkeyStr string) int {
func linkAccount(q *storage.Queries, fingerprint, pubkeyStr string) int {
	ctx := storage.Context()

	login, err := ask.GetLine("Enter your existing login: ")
	code, err := ask.GetLine("Enter your link code (from SSH LINK command): ")
	if err != nil {
		fmt.Println("Aborted.")
		return 1
	}
	login = strings.ToUpper(strings.TrimSpace(login))

	// Check user exists.
	existing, err := q.GetUser(ctx, login)
	if err != nil || existing.Login == "" {
		fmt.Println("ERROR: User not found.")
	code = strings.TrimSpace(code)
	if code == "" {
		fmt.Println("ERROR: No link code provided.")
		return 1
	}

	// Simple verification: ask for user's name.
	name, err := ask.GetLine("Enter the name on the account to verify: ")
	if err != nil {
		fmt.Println("Aborted.")
		return 1
	}
	if !strings.EqualFold(strings.TrimSpace(name), strings.TrimSpace(existing.Name)) {
		fmt.Println("ERROR: Name does not match. Access denied.")
	// Clean up expired codes first.
	_ = q.DeleteExpiredLinkCodes(ctx)

	// Look up the link code.
	linkCode, err := q.GetLinkCode(ctx, code)
	if err != nil || linkCode.Code == "" {
		fmt.Println("ERROR: Invalid or expired link code.")
		return 1
	}

	// Store the SSH key.
	if err := storeKey(q, fingerprint, pubkeyStr, login); err != nil {
	// Delete the code so it can't be reused.
	_ = q.DeleteLinkCode(ctx, code)

	// Store the SSH key for the linked account.
	if err := storeKey(q, fingerprint, pubkeyStr, linkCode.Login); err != nil {
		fmt.Printf("ERROR: Failed to store SSH key: %s\n", err)
		return 1
	}

	fmt.Printf("Key linked to account %s. Reconnect to start using BULLETIN.\n", login)
	fmt.Printf("Key linked to account %s. Reconnect to start using BULLETIN.\n", linkCode.Login)
	return 0
}

+20 −0
Original line number Diff line number Diff line
@@ -34,8 +34,28 @@ func readKey() (byte, error) {
	return buf[0], nil
}

// SanitizeText removes terminal escape sequences and dangerous control
// characters from text, while preserving UTF-8 and normal whitespace.
func SanitizeText(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		// Block ESC and other C0/C1 control characters except safe whitespace.
		if r == '\t' || r == '\n' || r == '\r' {
			b.WriteRune(r)
			continue
		}
		if r < 0x20 || (r >= 0x7f && r <= 0x9f) {
			continue
		}
		b.WriteRune(r)
	}
	return b.String()
}

// Pager pages through a string.
func Pager(content string) bool {
	content = SanitizeText(content)
	lines := strings.Split(content, "\n")
	totalLines := len(lines)

+29 −0
Original line number Diff line number Diff line
package repl

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
@@ -369,6 +371,33 @@ func ActionSSHDelete(cmd *dclish.Command) error {
	return nil
}

// ActionSSHLink handles the `SSH LINK` command. It generates a random
// link code that can be used during onboarding to link a new SSH key
// to the current user's account.
func ActionSSHLink(_ *dclish.Command) error {
	ctx := storage.Context()

	// Clean up expired codes first.
	_ = this.Q.DeleteExpiredLinkCodes(ctx)

	// Generate a random code.
	b := make([]byte, 10)
	if _, err := rand.Read(b); err != nil {
		return fmt.Errorf("failed to generate link code: %w", err)
	}
	code := hex.EncodeToString(b)

	// Store it.
	if err := this.Q.CreateLinkCode(ctx, code, this.User.Login); err != nil {
		return fmt.Errorf("failed to create link code: %w", err)
	}

	fmt.Printf("Your link code is: %s\n", code)
	fmt.Println("Use this code during onboarding to link a new SSH key to your account.")
	fmt.Println("This code expires in 15 minutes.")
	return nil
}

// ActionSSHFetch handles the `SSH FETCH` command.  This command pulls
// public keys from code sites.  It's the quickest way to
// add a number of keys for a user.  An admin can do this
Loading