appview/knots, appview/spindles: strip protocol and @ symbol from user inputs #748

merged
opened by evan.jarrett.net targeting master from evan.jarrett.net/core: input-sanitization

Summary#

Fixes input sanitization issues in spindle/knot registration and member management forms.

Changes#

  • Strip http://, https://, and trailing slashes from spindle/knot instance/domain inputs
  • Strip leading @ symbol from member inputs (add/remove) in both spindles and knots
  • Update form placeholders to show foo.bsky.social instead of @foo.bsky.social

Why#

Users naturally enter protocols (https://localhost:6555) or @ symbols (@evan.jarrett.net) based on UI placeholders, but the AT Protocol identity parser rejects these. This causes confusing failures during registration and member management.

Changed files
+21 -3
appview
knots
pages
templates
knots
repo
settings
spindles
spindles
+9
appview/knots/knots.go
··· 6 "log/slog" 7 "net/http" 8 "slices" 9 "time" 10 11 "github.com/go-chi/chi/v5" ··· 145 } 146 147 domain := r.FormValue("domain") 148 if domain == "" { 149 k.Pages.Notice(w, noticeId, "Incomplete form.") 150 return ··· 526 } 527 528 member := r.FormValue("member") 529 if member == "" { 530 l.Error("empty member") 531 k.Pages.Notice(w, noticeId, "Failed to add member, empty form.") ··· 626 } 627 628 member := r.FormValue("member") 629 if member == "" { 630 l.Error("empty member") 631 k.Pages.Notice(w, noticeId, "Failed to remove member, empty form.")
··· 6 "log/slog" 7 "net/http" 8 "slices" 9 + "strings" 10 "time" 11 12 "github.com/go-chi/chi/v5" ··· 146 } 147 148 domain := r.FormValue("domain") 149 + // Strip protocol, trailing slashes, and whitespace 150 + // Rkey cannot contain slashes 151 + domain = strings.TrimSpace(domain) 152 + domain = strings.TrimPrefix(domain, "https://") 153 + domain = strings.TrimPrefix(domain, "http://") 154 + domain = strings.TrimSuffix(domain, "/") 155 if domain == "" { 156 k.Pages.Notice(w, noticeId, "Incomplete form.") 157 return ··· 533 } 534 535 member := r.FormValue("member") 536 + member = strings.TrimPrefix(member, "@") 537 if member == "" { 538 l.Error("empty member") 539 k.Pages.Notice(w, noticeId, "Failed to add member, empty form.") ··· 634 } 635 636 member := r.FormValue("member") 637 + member = strings.TrimPrefix(member, "@") 638 if member == "" { 639 l.Error("empty member") 640 k.Pages.Notice(w, noticeId, "Failed to remove member, empty form.")
+1 -1
appview/pages/templates/knots/fragments/addMemberModal.html
··· 34 id="member-did-{{ .Id }}" 35 name="member" 36 required 37 - placeholder="@foo.bsky.social" 38 /> 39 <div class="flex gap-2 pt-2"> 40 <button
··· 34 id="member-did-{{ .Id }}" 35 name="member" 36 required 37 + placeholder="foo.bsky.social" 38 /> 39 <div class="flex gap-2 pt-2"> 40 <button
+1 -1
appview/pages/templates/repo/settings/access.html
··· 89 id="add-collaborator" 90 name="collaborator" 91 required 92 - placeholder="@foo.bsky.social" 93 /> 94 <div class="flex gap-2 pt-2"> 95 <button
··· 89 id="add-collaborator" 90 name="collaborator" 91 required 92 + placeholder="foo.bsky.social" 93 /> 94 <div class="flex gap-2 pt-2"> 95 <button
+1 -1
appview/pages/templates/spindles/fragments/addMemberModal.html
··· 36 id="member-did-{{ .Id }}" 37 name="member" 38 required 39 - placeholder="@foo.bsky.social" 40 /> 41 <div class="flex gap-2 pt-2"> 42 <button
··· 36 id="member-did-{{ .Id }}" 37 name="member" 38 required 39 + placeholder="foo.bsky.social" 40 /> 41 <div class="flex gap-2 pt-2"> 42 <button
+9
appview/spindles/spindles.go
··· 6 "log/slog" 7 "net/http" 8 "slices" 9 "time" 10 11 "github.com/go-chi/chi/v5" ··· 146 } 147 148 instance := r.FormValue("instance") 149 if instance == "" { 150 s.Pages.Notice(w, noticeId, "Incomplete form.") 151 return ··· 484 } 485 486 member := r.FormValue("member") 487 if member == "" { 488 l.Error("empty member") 489 s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") ··· 613 } 614 615 member := r.FormValue("member") 616 if member == "" { 617 l.Error("empty member") 618 s.Pages.Notice(w, noticeId, "Failed to remove member, empty form.")
··· 6 "log/slog" 7 "net/http" 8 "slices" 9 + "strings" 10 "time" 11 12 "github.com/go-chi/chi/v5" ··· 147 } 148 149 instance := r.FormValue("instance") 150 + // Strip protocol, trailing slashes, and whitespace 151 + // Rkey cannot contain slashes 152 + instance = strings.TrimSpace(instance) 153 + instance = strings.TrimPrefix(instance, "https://") 154 + instance = strings.TrimPrefix(instance, "http://") 155 + instance = strings.TrimSuffix(instance, "/") 156 if instance == "" { 157 s.Pages.Notice(w, noticeId, "Incomplete form.") 158 return ··· 491 } 492 493 member := r.FormValue("member") 494 + member = strings.TrimPrefix(member, "@") 495 if member == "" { 496 l.Error("empty member") 497 s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") ··· 621 } 622 623 member := r.FormValue("member") 624 + member = strings.TrimPrefix(member, "@") 625 if member == "" { 626 l.Error("empty member") 627 s.Pages.Notice(w, noticeId, "Failed to remove member, empty form.")