Enforce name limits server-side and reject hidden runes in model names
Security review follow-ups. The 200-character registry-name cap the UI forms promise now holds in domain.Validate for categories, tags, merchants and instruments, so a non-browser client cannot persist an unbounded name that every subsequent state response would carry. And a model-supplied merchant or taxonomy name containing control or format code points — bidi overrides, zero-width characters — is dropped like an identifier-shaped one: React escaping already prevented injection, but such names could visually spoof or reorder the review UI the operator approves from.
This commit is contained in:
@@ -250,6 +250,11 @@ func validHint(s string) bool {
|
||||
return utf8.ValidString(s) && utf8.RuneCountInString(s) <= 200
|
||||
}
|
||||
|
||||
// validName bounds registry display names at the 200 runes every UI form
|
||||
// already enforces, so no client can persist an unbounded name that every
|
||||
// later state response would carry.
|
||||
func validName(s string) bool { return nonblank(s) && utf8.RuneCountInString(s) <= 200 }
|
||||
|
||||
// ValidISIN reports a syntactically valid ISIN: two country letters, nine
|
||||
// alphanumerics and a check digit.
|
||||
func ValidISIN(s string) bool { return isinPattern.MatchString(s) }
|
||||
@@ -292,7 +297,7 @@ func Validate(d Dataset) error {
|
||||
if err := register(c.ID, "category"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !nonblank(c.Name) || !validHint(c.Hint) || (c.Kind != "expense" && c.Kind != "income") {
|
||||
if !validName(c.Name) || !validHint(c.Hint) || (c.Kind != "expense" && c.Kind != "income") {
|
||||
return fmt.Errorf("category %q: invalid name, hint or kind", c.ID)
|
||||
}
|
||||
categories[c.ID] = c
|
||||
@@ -330,7 +335,7 @@ func Validate(d Dataset) error {
|
||||
if err := register(t.ID, "tag"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !nonblank(t.Name) || !validHint(t.Hint) {
|
||||
if !validName(t.Name) || !validHint(t.Hint) {
|
||||
return fmt.Errorf("tag %q: name or hint invalid", t.ID)
|
||||
}
|
||||
tags[t.ID] = true
|
||||
@@ -339,8 +344,8 @@ func Validate(d Dataset) error {
|
||||
if err := register(m.ID, "merchant"); err != nil {
|
||||
return err
|
||||
}
|
||||
if !nonblank(m.Name) {
|
||||
return fmt.Errorf("merchant %q: name required", m.ID)
|
||||
if !validName(m.Name) {
|
||||
return fmt.Errorf("merchant %q: valid name of at most 200 characters required", m.ID)
|
||||
}
|
||||
if m.DefaultCategoryID != "" {
|
||||
if _, ok := categories[m.DefaultCategoryID]; !ok || children[m.DefaultCategoryID] {
|
||||
@@ -375,7 +380,7 @@ func Validate(d Dataset) error {
|
||||
if other, ok := isins[v.ISIN]; ok {
|
||||
return fmt.Errorf("instrument %q: ISIN %s already held by %q", v.ID, v.ISIN, other)
|
||||
}
|
||||
if !nonblank(v.Name) || !currencyPattern.MatchString(v.Currency) || !validText(v.Symbol) {
|
||||
if !validName(v.Name) || !currencyPattern.MatchString(v.Currency) || !validText(v.Symbol) {
|
||||
return fmt.Errorf("instrument %q: valid UTF-8 name and symbol and three-letter uppercase currency required", v.ID)
|
||||
}
|
||||
// A quote without its day cannot be judged stale, and a day without a
|
||||
|
||||
Reference in New Issue
Block a user