make sure every text input is safe and validated

Michael Muré created

fix #630

Change summary

api/graphql/resolvers/mutation.go | 39 +++++++++++++++++++++++++++-----
bridge/github/import.go           | 27 +++++----------------
bridge/gitlab/import.go           | 29 ++++++-----------------
bridge/jira/import.go             | 31 ++++++-------------------
bridge/launchpad/import.go        |  7 +++--
bug/label.go                      |  9 +-----
bug/op_create.go                  |  8 +-----
bug/op_set_metadata.go            | 11 +++++++++
bug/op_set_title.go               | 17 +++-----------
cache/bug_cache.go                |  2 +
commands/add.go                   |  6 ++++
commands/comment_add.go           |  3 +
commands/label_add.go             |  3 +
commands/label_rm.go              |  3 +
commands/title_edit.go            |  3 +
identity/version.go               | 22 +++++-------------
termui/termui.go                  | 12 ++++++---
util/text/transform.go            | 30 ++++++++++++++++++++++--
util/text/validate.go             | 12 ++++++++++
19 files changed, 147 insertions(+), 127 deletions(-)

Detailed changes

api/graphql/resolvers/mutation.go 🔗

@@ -5,11 +5,12 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/api/auth"
-	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/api/graphql/graph"
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 var _ graph.MutationResolver = &mutationResolver{}
@@ -50,7 +51,12 @@ func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput)
 		return nil, err
 	}
 
-	b, op, err := repo.NewBugRaw(author, time.Now().Unix(), input.Title, input.Message, input.Files, nil)
+	b, op, err := repo.NewBugRaw(author,
+		time.Now().Unix(),
+		text.CleanupOneLine(input.Title),
+		text.Cleanup(input.Message),
+		input.Files,
+		nil)
 	if err != nil {
 		return nil, err
 	}
@@ -73,7 +79,11 @@ func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommen
 		return nil, err
 	}
 
-	op, err := b.AddCommentRaw(author, time.Now().Unix(), input.Message, input.Files, nil)
+	op, err := b.AddCommentRaw(author,
+		time.Now().Unix(),
+		text.Cleanup(input.Message),
+		input.Files,
+		nil)
 	if err != nil {
 		return nil, err
 	}
@@ -101,7 +111,13 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm
 		return nil, err
 	}
 
-	op, err := b.EditCommentRaw(author, time.Now().Unix(), entity.Id(input.Target), input.Message, nil)
+	op, err := b.EditCommentRaw(
+		author,
+		time.Now().Unix(),
+		entity.Id(input.Target),
+		text.Cleanup(input.Message),
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}
@@ -129,7 +145,13 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.Change
 		return nil, err
 	}
 
-	results, op, err := b.ChangeLabelsRaw(author, time.Now().Unix(), input.Added, input.Removed, nil)
+	results, op, err := b.ChangeLabelsRaw(
+		author,
+		time.Now().Unix(),
+		text.CleanupOneLineArray(input.Added),
+		text.CleanupOneLineArray(input.Removed),
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}
@@ -219,7 +241,12 @@ func (r mutationResolver) SetTitle(ctx context.Context, input models.SetTitleInp
 		return nil, err
 	}
 
-	op, err := b.SetTitleRaw(author, time.Now().Unix(), input.Title, nil)
+	op, err := b.SetTitleRaw(
+		author,
+		time.Now().Unix(),
+		text.CleanupOneLine(input.Title),
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}

bridge/github/import.go 🔗

@@ -211,17 +211,13 @@ func (gi *githubImporter) ensureIssue(ctx context.Context, repo *cache.RepoCache
 		// if there are no issue edits then the issue struct holds the bug creation
 		textInput = string(issue.Body)
 	}
-	cleanText, err := text.Cleanup(textInput)
-	if err != nil {
-		return nil, err
-	}
 
 	// create bug
 	b, _, err = repo.NewBugRaw(
 		author,
 		issue.CreatedAt.Unix(),
-		title, // TODO: this is the *current* title, not the original one
-		cleanText,
+		text.CleanupOneLine(title), // TODO: this is the *current* title, not the original one
+		text.Cleanup(textInput),
 		nil,
 		map[string]string{
 			core.MetaKeyOrigin: target,
@@ -269,7 +265,7 @@ func (gi *githubImporter) ensureTimelineItem(ctx context.Context, repo *cache.Re
 			author,
 			item.LabeledEvent.CreatedAt.Unix(),
 			[]string{
-				string(item.LabeledEvent.Label.Name),
+				text.CleanupOneLine(string(item.LabeledEvent.Label.Name)),
 			},
 			nil,
 			map[string]string{metaKeyGithubId: id},
@@ -300,7 +296,7 @@ func (gi *githubImporter) ensureTimelineItem(ctx context.Context, repo *cache.Re
 			item.UnlabeledEvent.CreatedAt.Unix(),
 			nil,
 			[]string{
-				string(item.UnlabeledEvent.Label.Name),
+				text.CleanupOneLine(string(item.UnlabeledEvent.Label.Name)),
 			},
 			map[string]string{metaKeyGithubId: id},
 		)
@@ -382,7 +378,7 @@ func (gi *githubImporter) ensureTimelineItem(ctx context.Context, repo *cache.Re
 		// The title provided by the GraphQL API actually consists of a space followed
 		// by a zero width space (U+200B). This title would cause the NewBugRaw()
 		// function to return an error: empty title.
-		title := string(item.RenamedTitleEvent.CurrentTitle)
+		title := text.CleanupOneLine(string(item.RenamedTitleEvent.CurrentTitle))
 		if title == " \u200b" { // U+200B == zero width space
 			title = EmptyTitlePlaceholder
 		}
@@ -429,17 +425,12 @@ func (gi *githubImporter) ensureCommentEdit(ctx context.Context, repo *cache.Rep
 		return nil
 	}
 
-	cleanText, err := text.Cleanup(string(*edit.Diff))
-	if err != nil {
-		return err
-	}
-
 	// comment edition
 	op, err := b.EditCommentRaw(
 		editor,
 		edit.CreatedAt.Unix(),
 		target,
-		cleanText,
+		text.Cleanup(string(*edit.Diff)),
 		map[string]string{
 			metaKeyGithubId: parseId(edit.Id),
 		},
@@ -476,16 +467,12 @@ func (gi *githubImporter) ensureComment(ctx context.Context, repo *cache.RepoCac
 		// if there are not comment edits, then the comment struct holds the comment creation
 		textInput = string(comment.Body)
 	}
-	cleanText, err := text.Cleanup(textInput)
-	if err != nil {
-		return err
-	}
 
 	// add comment operation
 	op, err := b.AddCommentRaw(
 		author,
 		comment.CreatedAt.Unix(),
-		cleanText,
+		text.Cleanup(textInput),
 		nil,
 		map[string]string{
 			metaKeyGithubId:  parseId(comment.Id),

bridge/gitlab/import.go 🔗

@@ -137,22 +137,12 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
 		return nil, err
 	}
 
-	// if bug was never imported
-	cleanTitle, err := text.Cleanup(issue.Title)
-	if err != nil {
-		return nil, err
-	}
-	cleanDesc, err := text.Cleanup(issue.Description)
-	if err != nil {
-		return nil, err
-	}
-
-	// create bug
+	// if bug was never imported, create bug
 	b, _, err = repo.NewBugRaw(
 		author,
 		issue.CreatedAt.Unix(),
-		cleanTitle,
-		cleanDesc,
+		text.CleanupOneLine(issue.Title),
+		text.Cleanup(issue.Description),
 		nil,
 		map[string]string{
 			core.MetaKeyOrigin:   target,
@@ -238,7 +228,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
 				author,
 				note.UpdatedAt.Unix(),
 				firstComment.Id(),
-				issue.Description,
+				text.Cleanup(issue.Description),
 				map[string]string{
 					metaKeyGitlabId: gitlabID,
 				},
@@ -251,10 +241,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
 		}
 
 	case NOTE_COMMENT:
-		cleanText, err := text.Cleanup(body)
-		if err != nil {
-			return err
-		}
+		cleanText := text.Cleanup(body)
 
 		// if we didn't import the comment
 		if errResolve == cache.ErrNoMatchingOp {
@@ -312,7 +299,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
 		op, err := b.SetTitleRaw(
 			author,
 			note.CreatedAt.Unix(),
-			body,
+			text.CleanupOneLine(body),
 			map[string]string{
 				metaKeyGitlabId: gitlabID,
 			},
@@ -361,7 +348,7 @@ func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCa
 		_, err = b.ForceChangeLabelsRaw(
 			author,
 			labelEvent.CreatedAt.Unix(),
-			[]string{labelEvent.Label.Name},
+			[]string{text.CleanupOneLine(labelEvent.Label.Name)},
 			nil,
 			map[string]string{
 				metaKeyGitlabId: parseID(labelEvent.ID),
@@ -373,7 +360,7 @@ func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCa
 			author,
 			labelEvent.CreatedAt.Unix(),
 			nil,
-			[]string{labelEvent.Label.Name},
+			[]string{text.CleanupOneLine(labelEvent.Label.Name)},
 			map[string]string{
 				metaKeyGitlabId: parseID(labelEvent.ID),
 			},

bridge/jira/import.go 🔗

@@ -232,19 +232,11 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
 	}
 
 	if err == bug.ErrBugNotExist {
-		cleanText, err := text.Cleanup(string(issue.Fields.Description))
-		if err != nil {
-			return nil, err
-		}
-
-		// NOTE(josh): newlines in titles appears to be rare, but it has been seen
-		// in the wild. It does not appear to be allowed in the JIRA web interface.
-		title := strings.Replace(issue.Fields.Summary, "\n", "", -1)
 		b, _, err = repo.NewBugRaw(
 			author,
 			issue.Fields.Created.Unix(),
-			title,
-			cleanText,
+			text.CleanupOneLine(issue.Fields.Summary),
+			text.Cleanup(issue.Fields.Description),
 			nil,
 			map[string]string{
 				core.MetaKeyOrigin: target,
@@ -289,10 +281,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 			// We don't know the original text... we only have the updated text.
 			cleanText = ""
 		} else {
-			cleanText, err = text.Cleanup(string(item.Body))
-			if err != nil {
-				return err
-			}
+			cleanText = text.Cleanup(item.Body)
 		}
 
 		// add comment operation
@@ -340,15 +329,11 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 	}
 
 	// comment edition
-	cleanText, err := text.Cleanup(string(item.Body))
-	if err != nil {
-		return err
-	}
 	op, err := b.EditCommentRaw(
 		editor,
 		item.Updated.Unix(),
 		targetOpID,
-		cleanText,
+		text.Cleanup(item.Body),
 		map[string]string{
 			metaKeyJiraId: derivedID,
 		},
@@ -513,8 +498,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			op, err := b.ForceChangeLabelsRaw(
 				author,
 				entry.Created.Unix(),
-				addedLabels,
-				removedLabels,
+				text.CleanupOneLineArray(addedLabels),
+				text.CleanupOneLineArray(removedLabels),
 				map[string]string{
 					metaKeyJiraId:        entry.ID,
 					metaKeyJiraDerivedId: derivedID,
@@ -571,7 +556,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			op, err := b.SetTitleRaw(
 				author,
 				entry.Created.Unix(),
-				string(item.ToString),
+				text.CleanupOneLine(item.ToString),
 				map[string]string{
 					metaKeyJiraId:        entry.ID,
 					metaKeyJiraDerivedId: derivedID,
@@ -589,7 +574,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			op, err := b.EditCreateCommentRaw(
 				author,
 				entry.Created.Unix(),
-				string(item.ToString),
+				text.Cleanup(item.ToString),
 				map[string]string{
 					metaKeyJiraId:        entry.ID,
 					metaKeyJiraDerivedId: derivedID,

bridge/launchpad/import.go 🔗

@@ -9,6 +9,7 @@ import (
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type launchpadImporter struct {
@@ -83,8 +84,8 @@ func (li *launchpadImporter) ImportAll(ctx context.Context, repo *cache.RepoCach
 					b, _, err = repo.NewBugRaw(
 						owner,
 						createdAt.Unix(),
-						lpBug.Title,
-						lpBug.Description,
+						text.CleanupOneLine(lpBug.Title),
+						text.Cleanup(lpBug.Description),
 						nil,
 						map[string]string{
 							core.MetaKeyOrigin: target,
@@ -133,7 +134,7 @@ func (li *launchpadImporter) ImportAll(ctx context.Context, repo *cache.RepoCach
 					op, err := b.AddCommentRaw(
 						owner,
 						createdAt.Unix(),
-						lpMessage.Content,
+						text.Cleanup(lpMessage.Content),
 						nil,
 						map[string]string{
 							metaKeyLaunchpadID: lpMessage.ID,

bug/label.go 🔗

@@ -4,7 +4,6 @@ import (
 	"crypto/sha256"
 	"fmt"
 	"image/color"
-	"strings"
 
 	fcolor "github.com/fatih/color"
 
@@ -58,12 +57,8 @@ func (l Label) Validate() error {
 		return fmt.Errorf("empty")
 	}
 
-	if strings.Contains(str, "\n") {
-		return fmt.Errorf("should be a single line")
-	}
-
-	if !text.Safe(str) {
-		return fmt.Errorf("not fully printable")
+	if !text.SafeOneLine(str) {
+		return fmt.Errorf("label has unsafe characters")
 	}
 
 	return nil

bug/op_create.go 🔗

@@ -3,7 +3,6 @@ package bug
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
@@ -94,11 +93,8 @@ func (op *CreateOperation) Validate() error {
 	if text.Empty(op.Title) {
 		return fmt.Errorf("title is empty")
 	}
-	if strings.Contains(op.Title, "\n") {
-		return fmt.Errorf("title should be a single line")
-	}
-	if !text.Safe(op.Title) {
-		return fmt.Errorf("title is not fully printable")
+	if !text.SafeOneLine(op.Title) {
+		return fmt.Errorf("title has unsafe characters")
 	}
 
 	if !text.Safe(op.Message) {

bug/op_set_metadata.go 🔗

@@ -2,11 +2,13 @@ package bug
 
 import (
 	"encoding/json"
+	"fmt"
 
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 var _ Operation = &SetMetadataOperation{}
@@ -43,6 +45,15 @@ func (op *SetMetadataOperation) Validate() error {
 		return errors.Wrap(err, "target invalid")
 	}
 
+	for key, val := range op.NewMetadata {
+		if !text.SafeOneLine(key) {
+			return fmt.Errorf("metadata key is unsafe")
+		}
+		if !text.Safe(val) {
+			return fmt.Errorf("metadata value is not fully printable")
+		}
+	}
+
 	return nil
 }
 

bug/op_set_title.go 🔗

@@ -3,7 +3,6 @@ package bug
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
@@ -49,20 +48,12 @@ func (op *SetTitleOperation) Validate() error {
 		return fmt.Errorf("title is empty")
 	}
 
-	if strings.Contains(op.Title, "\n") {
-		return fmt.Errorf("title should be a single line")
+	if !text.SafeOneLine(op.Title) {
+		return fmt.Errorf("title has unsafe characters")
 	}
 
-	if !text.Safe(op.Title) {
-		return fmt.Errorf("title should be fully printable")
-	}
-
-	if strings.Contains(op.Was, "\n") {
-		return fmt.Errorf("previous title should be a single line")
-	}
-
-	if !text.Safe(op.Was) {
-		return fmt.Errorf("previous title should be fully printable")
+	if !text.SafeOneLine(op.Was) {
+		return fmt.Errorf("previous title has unsafe characters")
 	}
 
 	return nil

cache/bug_cache.go 🔗

@@ -235,6 +235,7 @@ func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title stri
 	return op, c.notifyUpdated()
 }
 
+// Convenience function to edit the body of a bug (the first comment)
 func (c *BugCache) EditCreateComment(body string) (*bug.EditCommentOperation, error) {
 	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
@@ -244,6 +245,7 @@ func (c *BugCache) EditCreateComment(body string) (*bug.EditCommentOperation, er
 	return c.EditCreateCommentRaw(author, time.Now().Unix(), body, nil)
 }
 
+// Convenience function to edit the body of a bug (the first comment)
 func (c *BugCache) EditCreateCommentRaw(author *IdentityCache, unixTime int64, body string, metadata map[string]string) (*bug.EditCommentOperation, error) {
 	c.mu.Lock()
 	op, err := bug.EditCreateComment(c.bug, author.Identity, unixTime, body)

commands/add.go 🔗

@@ -4,6 +4,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type addOptions struct {
@@ -60,7 +61,10 @@ func runAdd(env *Env, opts addOptions) error {
 		}
 	}
 
-	b, _, err := env.backend.NewBug(opts.title, opts.message)
+	b, _, err := env.backend.NewBug(
+		text.CleanupOneLine(opts.title),
+		text.Cleanup(opts.message),
+	)
 	if err != nil {
 		return err
 	}

commands/comment_add.go 🔗

@@ -5,6 +5,7 @@ import (
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type commentAddOptions struct {
@@ -62,7 +63,7 @@ func runCommentAdd(env *Env, opts commentAddOptions, args []string) error {
 		}
 	}
 
-	_, err = b.AddComment(opts.message)
+	_, err = b.AddComment(text.Cleanup(opts.message))
 	if err != nil {
 		return err
 	}

commands/label_add.go 🔗

@@ -4,6 +4,7 @@ import (
 	"github.com/spf13/cobra"
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 func newLabelAddCommand() *cobra.Command {
@@ -30,7 +31,7 @@ func runLabelAdd(env *Env, args []string) error {
 
 	added := args
 
-	changes, _, err := b.ChangeLabels(added, nil)
+	changes, _, err := b.ChangeLabels(text.CleanupOneLineArray(added), nil)
 
 	for _, change := range changes {
 		env.out.Println(change)

commands/label_rm.go 🔗

@@ -4,6 +4,7 @@ import (
 	"github.com/spf13/cobra"
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 func newLabelRmCommand() *cobra.Command {
@@ -30,7 +31,7 @@ func runLabelRm(env *Env, args []string) error {
 
 	removed := args
 
-	changes, _, err := b.ChangeLabels(nil, removed)
+	changes, _, err := b.ChangeLabels(nil, text.CleanupOneLineArray(removed))
 
 	for _, change := range changes {
 		env.out.Println(change)

commands/title_edit.go 🔗

@@ -5,6 +5,7 @@ import (
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type titleEditOptions struct {
@@ -58,7 +59,7 @@ func runTitleEdit(env *Env, opts titleEditOptions, args []string) error {
 		env.err.Println("No change, aborting.")
 	}
 
-	_, err = b.SetTitle(opts.title)
+	_, err = b.SetTitle(text.CleanupOneLine(opts.title))
 	if err != nil {
 		return err
 	}

identity/version.go 🔗

@@ -4,7 +4,6 @@ import (
 	"crypto/rand"
 	"encoding/json"
 	"fmt"
-	"strings"
 	"time"
 
 	"github.com/pkg/errors"
@@ -186,25 +185,16 @@ func (v *version) Validate() error {
 	if text.Empty(v.name) && text.Empty(v.login) {
 		return fmt.Errorf("either name or login should be set")
 	}
-	if strings.Contains(v.name, "\n") {
-		return fmt.Errorf("name should be a single line")
-	}
-	if !text.Safe(v.name) {
-		return fmt.Errorf("name is not fully printable")
+	if !text.SafeOneLine(v.name) {
+		return fmt.Errorf("name has unsafe characters")
 	}
 
-	if strings.Contains(v.login, "\n") {
-		return fmt.Errorf("login should be a single line")
-	}
-	if !text.Safe(v.login) {
-		return fmt.Errorf("login is not fully printable")
+	if !text.SafeOneLine(v.login) {
+		return fmt.Errorf("login has unsafe characters")
 	}
 
-	if strings.Contains(v.email, "\n") {
-		return fmt.Errorf("email should be a single line")
-	}
-	if !text.Safe(v.email) {
-		return fmt.Errorf("email is not fully printable")
+	if !text.SafeOneLine(v.email) {
+		return fmt.Errorf("email has unsafe characters")
 	}
 
 	if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {

termui/termui.go 🔗

@@ -11,6 +11,7 @@ import (
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/query"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 var errTerminateMainloop = errors.New("terminate gocui mainloop")
@@ -199,7 +200,10 @@ func newBugWithEditor(repo *cache.RepoCache) error {
 
 		return errTerminateMainloop
 	} else {
-		b, _, err = repo.NewBug(title, message)
+		b, _, err = repo.NewBug(
+			text.CleanupOneLine(title),
+			text.Cleanup(message),
+		)
 		if err != nil {
 			return err
 		}
@@ -235,7 +239,7 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	if err == input.ErrEmptyMessage {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
 	} else {
-		_, err := bug.AddComment(message)
+		_, err := bug.AddComment(text.Cleanup(message))
 		if err != nil {
 			return err
 		}
@@ -270,7 +274,7 @@ func editCommentWithEditor(bug *cache.BugCache, target entity.Id, preMessage str
 	} else if message == preMessage {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.")
 	} else {
-		_, err := bug.EditComment(target, message)
+		_, err := bug.EditComment(target, text.Cleanup(message))
 		if err != nil {
 			return err
 		}
@@ -307,7 +311,7 @@ func setTitleWithEditor(bug *cache.BugCache) error {
 	} else if title == snap.Title {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
 	} else {
-		_, err := bug.SetTitle(title)
+		_, err := bug.SetTitle(text.CleanupOneLine(title))
 		if err != nil {
 			return err
 		}

util/text/transform.go 🔗

@@ -8,7 +8,7 @@ import (
 	"golang.org/x/text/transform"
 )
 
-func Cleanup(text string) (string, error) {
+func Cleanup(text string) string {
 	// windows new line, Github, really ?
 	text = strings.Replace(text, "\r\n", "\n", -1)
 
@@ -23,9 +23,33 @@ func Cleanup(text string) (string, error) {
 	}))
 	sanitized, _, err := transform.String(t, text)
 	if err != nil {
-		return "", err
+		// transform.String should never return an error as our transformer doesn't returns one.
+		// Confirmed with fuzzing.
+		panic(err)
 	}
 
 	// trim extra new line not displayed in the github UI but still present in the data
-	return strings.TrimSpace(sanitized), nil
+	return strings.TrimSpace(sanitized)
+}
+
+func CleanupOneLine(text string) string {
+	// remove all unicode control characters *including*
+	// '\n', '\r' and '\t'
+	t := runes.Remove(runes.Predicate(unicode.IsControl))
+	sanitized, _, err := transform.String(t, text)
+	if err != nil {
+		// transform.String should never return an error as our transformer doesn't returns one.
+		// Confirmed with fuzzing.
+		panic(err)
+	}
+
+	// trim extra new line not displayed in the github UI but still present in the data
+	return strings.TrimSpace(sanitized)
+}
+
+func CleanupOneLineArray(texts []string) []string {
+	for i := range texts {
+		texts[i] = CleanupOneLine(texts[i])
+	}
+	return texts
 }

util/text/validate.go 🔗

@@ -33,6 +33,18 @@ func Safe(s string) bool {
 	return true
 }
 
+// Safe will tell if a character in the string is considered unsafe
+// Currently trigger on all unicode control character
+func SafeOneLine(s string) bool {
+	for _, r := range s {
+		if unicode.IsControl(r) {
+			return false
+		}
+	}
+
+	return true
+}
+
 // ValidUrl will tell if the string contains what seems to be a valid URL
 func ValidUrl(s string) bool {
 	if strings.Contains(s, "\n") {