Commit 71acfef8 authored by Kevin Lyda's avatar Kevin Lyda
Browse files

Reduce a number of memory vulnerabilities

Use memory more efficiently and prevent users from doing things that
would over-allocate memory.
parent ae7d569b
Loading
Loading
Loading
Loading
+80 −0
Original line number Diff line number Diff line
@@ -2,14 +2,36 @@ package folders

import (
	"errors"
	"fmt"
	"time"
	"unsafe"

	"git.lyda.ie/pp/bulletin/storage"
	"git.lyda.ie/pp/bulletin/this"
)

// MaxMessageSize is the maximum size of a message body in bytes (64 KB).
const MaxMessageSize = 64 * 1024

// MaxSubjectSize is the maximum size of a subject in bytes.
const MaxSubjectSize = 256

// ValidateMessageSize checks that a message and subject are within size limits.
func ValidateMessageSize(subject, message string) error {
	if len(subject) > MaxSubjectSize {
		return fmt.Errorf("subject too long (%d bytes, max %d)", len(subject), MaxSubjectSize)
	}
	if len(message) > MaxMessageSize {
		return fmt.Errorf("message too long (%d bytes, max %d)", len(message), MaxMessageSize)
	}
	return nil
}

// CreateMessage creates a new message.
func CreateMessage(author, subject, message, folder string, permanent, system, shutdown int, expiration *time.Time) error {
	if err := ValidateMessageSize(subject, message); err != nil {
		return err
	}
	ctx := storage.Context()
	if expiration == nil {
		days, err := this.Q.GetFolderExpire(ctx, folder)
@@ -173,6 +195,64 @@ func ListMessages(folder string) ([]storage.Message, error) {
	return rows, err
}

// dirRow is the interface for Dir query row types that have the same
// fields as Message.
type dirRow interface {
	storage.DirMessagesRow | storage.DirMessagesSeenRow |
		storage.DirMessagesUnseenRow | storage.DirMessagesMarkedRow |
		storage.DirMessagesUnmarkedRow |
		storage.DirSearchRow | storage.DirSearchSubjectRow |
		storage.DirSearchReplyRow
}

// ToMessages converts Dir query rows to Messages. Exported for use in repl.
func ToMessages[T dirRow](rows []T) []storage.Message {
	return toMessages(rows)
}

func toMessages[T dirRow](rows []T) []storage.Message {
	msgs := make([]storage.Message, len(rows))
	for i := range rows {
		msgs[i] = *(*storage.Message)(unsafe.Pointer(&rows[i])) // #nosec G103 -- types are layout-identical
	}
	return msgs
}

// DirMessages lists messages without message bodies.
func DirMessages(folder string) ([]storage.Message, error) {
	ctx := storage.Context()
	rows, err := this.Q.DirMessages(ctx, folder)
	return toMessages(rows), err
}

// DirMessagesSeen lists seen messages without message bodies.
func DirMessagesSeen(login, folder string) ([]storage.Message, error) {
	ctx := storage.Context()
	rows, err := this.Q.DirMessagesSeen(ctx, folder, login)
	return toMessages(rows), err
}

// DirMessagesUnseen lists unseen messages without message bodies.
func DirMessagesUnseen(login, folder string) ([]storage.Message, error) {
	ctx := storage.Context()
	rows, err := this.Q.DirMessagesUnseen(ctx, folder, login)
	return toMessages(rows), err
}

// DirMessagesMarked lists marked messages without message bodies.
func DirMessagesMarked(login, folder string) ([]storage.Message, error) {
	ctx := storage.Context()
	rows, err := this.Q.DirMessagesMarked(ctx, folder, login)
	return toMessages(rows), err
}

// DirMessagesUnmarked lists unmarked messages without message bodies.
func DirMessagesUnmarked(login, folder string) ([]storage.Message, error) {
	ctx := storage.Context()
	rows, err := this.Q.DirMessagesUnmarked(ctx, folder, login)
	return toMessages(rows), err
}

// WroteAllMessages returns true if login wrote all msgids
func WroteAllMessages(login string, msgids []int64) bool {
	ctx := storage.Context()
+27 −9
Original line number Diff line number Diff line
@@ -13,6 +13,7 @@ import (
	"os"
	"path"
	"strings"
	"time"

	"git.lyda.ie/pp/bulletin/storage"
	"github.com/adrg/xdg"
@@ -60,14 +61,25 @@ func addToFile(login, public string) error {
	}
	defer unix.Flock(fd, unix.LOCK_UN) //nolint:errcheck // unlock after we're done

	// Check for duplicates.
	keycontent, err := io.ReadAll(f)
	// Check for duplicates using a scanner.
	newKeyBytes := theKey.Marshal()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := bytes.TrimSpace(scanner.Bytes())
		if len(line) == 0 {
			continue
		}
		existing, _, _, _, err := ssh.ParseAuthorizedKey(line)
		if err != nil {
		return err
			continue
		}
	if bytes.Contains(keycontent, ssh.MarshalAuthorizedKey(theKey)) {
		if bytes.Equal(existing.Marshal(), newKeyBytes) {
			return ErrDuplicateKey
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}

	// Generate and write the key.
	keyline := fmt.Sprintf(keytemplate, bulletin, login, string(ssh.MarshalAuthorizedKey(theKey)))
@@ -225,16 +237,22 @@ func FetchDB(q *storage.Queries, login, nickname, username string) string {
	if site == "" {
		return fmt.Sprintln("ERROR: site nickname unknown.")
	}
	url := fmt.Sprintf(site, username)
	resp, err := http.Get(url) // #nosec G107 -- URL is constructed from a hardcoded allowlist of sites
	fetchURL := fmt.Sprintf(site, username)
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Get(fetchURL) // #nosec G107 -- URL is constructed from a hardcoded allowlist of sites
	if err != nil {
		return fmt.Sprintf("ERROR: Failed to fetch ssh keys (%s).\n", err)
	}
	defer resp.Body.Close()
	scanner := bufio.NewScanner(resp.Body)
	const maxBody = 1024 * 1024 // 1 MB
	const maxKeys = 100
	scanner := bufio.NewScanner(io.LimitReader(resp.Body, maxBody))
	keys := 0
	dups := 0
	for scanner.Scan() {
		if keys+dups >= maxKeys {
			return fmt.Sprintf("ERROR: Too many keys (max %d).\n", maxKeys)
		}
		keyline := string(bytes.TrimSpace(scanner.Bytes()))
		if err := AddDB(q, strings.ToUpper(login), keyline); err != nil {
			if errors.Is(err, ErrDuplicateKey) {
+7 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ import (
	"git.lyda.ie/pp/bulletin/ask"
	"git.lyda.ie/pp/bulletin/dclish"
	"git.lyda.ie/pp/bulletin/editor"
	"git.lyda.ie/pp/bulletin/folders"
	"git.lyda.ie/pp/bulletin/storage"
	"git.lyda.ie/pp/bulletin/this"
	"github.com/adrg/xdg"
@@ -127,6 +128,9 @@ func ActionMail(cmd *dclish.Command) error {
		return err
	}

	if err := folders.ValidateMessageSize(subject, message); err != nil {
		return err
	}
	for _, recipient := range recipients {
		err = this.Q.CreateMail(ctx, storage.CreateMailParams{
			FromLogin: this.User.Login,
@@ -244,6 +248,9 @@ func ActionRespond(cmd *dclish.Command) error {
		}
	}

	if err := folders.ValidateMessageSize(subject, message); err != nil {
		return err
	}
	err = this.Q.CreateMail(ctx, storage.CreateMailParams{
		FromLogin: this.User.Login,
		ToLogin:   recipient,
+7 −0
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ import (
	"git.lyda.ie/pp/bulletin/ask"
	"git.lyda.ie/pp/bulletin/dclish"
	"git.lyda.ie/pp/bulletin/editor"
	"git.lyda.ie/pp/bulletin/folders"
	"git.lyda.ie/pp/bulletin/pager"
	"git.lyda.ie/pp/bulletin/storage"
	"git.lyda.ie/pp/bulletin/this"
@@ -104,6 +105,9 @@ func ActionMailSend(cmd *dclish.Command) error {
		return err
	}

	if err := folders.ValidateMessageSize(subject, message); err != nil {
		return err
	}
	for _, recipient := range recipients {
		err = this.Q.CreateMail(ctx, storage.CreateMailParams{
			FromLogin: this.User.Login,
@@ -238,6 +242,9 @@ func ActionMailReply(_ *dclish.Command) error {
		return err
	}

	if err := folders.ValidateMessageSize(subject, message); err != nil {
		return err
	}
	err = this.Q.CreateMail(ctx, storage.CreateMailParams{
		FromLogin: this.User.Login,
		ToLogin:   mail.FromLogin,
+48 −17
Original line number Diff line number Diff line
@@ -67,23 +67,56 @@ func ActionDirectory(cmd *dclish.Command) error {
	}
	// Get base message list based on filter flags.
	ctx := storage.Context()
	needBody := cmd.Flags["/PRINT"].Value == "true"
	var msgs []storage.Message
	var err error
	switch {
	case cmd.Flags["/ALL"].Set:
		if needBody {
			msgs, err = folders.ListMessages(this.Folder.Name)
		} else {
			msgs, err = folders.DirMessages(this.Folder.Name)
		}
	case cmd.Flags["/MARKED"].Set:
		if needBody {
			msgs, err = folders.ListMessagesMarked(this.User.Login, this.Folder.Name)
		} else {
			msgs, err = folders.DirMessagesMarked(this.User.Login, this.Folder.Name)
		}
	case cmd.Flags["/UNMARKED"].Set:
		if needBody {
			msgs, err = folders.ListMessagesUnmarked(this.User.Login, this.Folder.Name)
		} else {
			msgs, err = folders.DirMessagesUnmarked(this.User.Login, this.Folder.Name)
		}
	case cmd.Flags["/SEEN"].Set:
		if needBody {
			msgs, err = folders.ListMessagesSeen(this.User.Login, this.Folder.Name)
		} else {
			msgs, err = folders.DirMessagesSeen(this.User.Login, this.Folder.Name)
		}
	case cmd.Flags["/NEW"].Set || cmd.Flags["/UNSEEN"].Set:
		if needBody {
			msgs, err = folders.ListMessagesUnseen(this.User.Login, this.Folder.Name)
		} else {
			msgs, err = folders.DirMessagesUnseen(this.User.Login, this.Folder.Name)
		}
	case cmd.Flags["/SEARCH"].Set:
		if needBody {
			msgs, err = this.Q.Search(ctx, nullStr(cmd.Flags["/SEARCH"].Value), 1, this.Folder.Name)
		} else {
			var rows []storage.DirSearchRow
			rows, err = this.Q.DirSearch(ctx, nullStr(cmd.Flags["/SEARCH"].Value), 1, this.Folder.Name)
			msgs = folders.ToMessages(rows)
		}
	case cmd.Flags["/SUBJECT"].Set:
		if needBody {
			msgs, err = this.Q.SearchSubject(ctx, nullStr(cmd.Flags["/SUBJECT"].Value), 1, this.Folder.Name)
		} else {
			var rows []storage.DirSearchSubjectRow
			rows, err = this.Q.DirSearchSubject(ctx, nullStr(cmd.Flags["/SUBJECT"].Value), 1, this.Folder.Name)
			msgs = folders.ToMessages(rows)
		}
	case cmd.Flags["/REPLY"].Set:
		if this.MsgID == 0 {
			fmt.Println("No current message for /REPLY.")
@@ -94,17 +127,15 @@ func ActionDirectory(cmd *dclish.Command) error {
			return e
		}
		replySubj := "Re: " + curMsg.Subject
		all, e := folders.ListMessages(this.Folder.Name)
		if e != nil {
			return e
		}
		for _, m := range all {
			if strings.HasPrefix(m.Subject, replySubj) {
				msgs = append(msgs, m)
			}
		}
		var rows []storage.DirSearchReplyRow
		rows, err = this.Q.DirSearchReply(ctx, nullStr(replySubj), this.Folder.Name)
		msgs = folders.ToMessages(rows)
	default:
		if needBody {
			msgs, err = folders.ListMessages(this.Folder.Name)
		} else {
			msgs, err = folders.DirMessages(this.Folder.Name)
		}
	}
	if err != nil {
		return err
Loading