Commit 1a274bec3b

1a274bec3b80f5a4b56858756bb0b9bea528d3cc

parent: b53740ec50

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-12 04:46 UTC

web: resolve a snippet before checking its confirmation

snippetDeleteSubmit and snippetFileRemoveSubmit checked confirmed()
before snippetScope resolved the snippet, so an unconfirmed post to a
snippet the viewer may not read got a flash redirect instead of the
404 every other path gives; snippetAction now takes the resolved
snippet so each caller can scope first. Also route labels.go's empty
name refusal through its back closure and add prefix8() to
internal/httpd/confirm.go to replace four inline 8-character
truncations in account.go.

Ref #182
e2e/snippetweb_test.go +15
@@ -138,6 +138,15 @@ func TestSnippetsWeb(t *testing.T) {
138138 if status, _ := browserPost(t, alice, page+"/file", url.Values{"name": {"b.txt"}, "content": {"b\n"}}); status != 200 {
139139 t.Fatal("file add failed")
140140 }
141 // Removing a file needs its name typed; a bare post is refused and
142 // the file stays.
143 _, body = browserPost(t, alice, page+"/file/remove", url.Values{"name": {"b.txt"}})
144 if !strings.Contains(body, "type b.txt to confirm") {
145 t.Fatalf("unconfirmed file remove was not refused:\n%s", body)
146 }
147 if _, _, code := inst.ssh(t, aliceKey, "", "snippet", "file", "get", created, "b.txt"); code != 0 {
148 t.Fatalf("b.txt removed without confirmation: exit %d", code)
149 }
141150 if status, _ := browserPost(t, alice, page+"/file/remove", url.Values{"name": {"b.txt"}, "confirm": {"b.txt"}}); status != 200 {
142151 t.Fatal("file remove failed")
143152 }
@@ -169,6 +178,12 @@ func TestSnippetsWeb(t *testing.T) {
169178 if status, _ := browserGet(t, bob, inst.base()+"/alice/-/snippets/"+unlisted); status != 200 {
170179 t.Fatalf("stranger on an unlisted page: %d", status)
171180 }
181 // An unconfirmed delete on a private snippet is still 404 for a
182 // stranger: the snippet is resolved, and refused, before the
183 // confirmation is even checked.
184 if status, _ := browserPost(t, bob, inst.base()+"/alice/-/snippets/"+private+"/delete", nil); status != 404 {
185 t.Fatalf("stranger's unconfirmed delete on a private snippet: %d", status)
186 }
172187 if status, _ := browserPost(t, bob, inst.base()+"/alice/-/snippets/"+public+"/edit", url.Values{"description": {"x"}, "visibility": {"public"}}); status != 403 {
173188 t.Fatalf("bob editing alice's snippet: %d", status)
174189 }
internal/httpd/account.go +4 −16
@@ -37,10 +37,7 @@ func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.Use
3737 var keys []accountKey
3838 if list, err := s.st.ListSSHKeys(u.ID); err == nil {
3939 for _, k := range list {
40 confirm := strings.TrimPrefix(k.Fingerprint, "SHA256:")
41 if len(confirm) > 8 {
42 confirm = confirm[:8]
43 }
40 confirm := prefix8(strings.TrimPrefix(k.Fingerprint, "SHA256:"))
4441 keys = append(keys, accountKey{Fingerprint: k.Fingerprint, Algo: k.Algo, Scope: k.Scope, Label: k.Label, Confirm: confirm})
4542 }
4643 }
@@ -49,10 +46,7 @@ func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.Use
4946 for _, k := range list {
5047 var uids []string
5148 json.Unmarshal([]byte(k.UIDsJSON), &uids)
52 confirm := k.Fingerprint
53 if len(confirm) > 8 {
54 confirm = confirm[:8]
55 }
49 confirm := prefix8(k.Fingerprint)
5650 pgp = append(pgp, accountPGP{
5751 Fingerprint: k.Fingerprint, UIDs: uids,
5852 Expired: k.ExpiresAt != nil, Revoked: k.RevokedAt != nil, Confirm: confirm,
@@ -163,10 +157,7 @@ func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.U
163157 }
164158 back("", "key registered")
165159 case "key-remove":
166 want := strings.TrimPrefix(r.FormValue("fingerprint"), "SHA256:")
167 if len(want) > 8 {
168 want = want[:8]
169 }
160 want := prefix8(strings.TrimPrefix(r.FormValue("fingerprint"), "SHA256:"))
170161 if ok, msg := confirmed(r, want); !ok {
171162 back(msg, "")
172163 return
@@ -189,10 +180,7 @@ func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.U
189180 back("", "PGP key registered")
190181 case "pgp-remove":
191182 fp := r.FormValue("fingerprint")
192 want := fp
193 if len(want) > 8 {
194 want = want[:8]
195 }
183 want := prefix8(fp)
196184 if ok, msg := confirmed(r, want); !ok {
197185 back(msg, "")
198186 return
internal/httpd/confirm.go +9
@@ -15,3 +15,12 @@ func confirmed(r *http.Request, want string) (bool, string) {
1515 }
1616 return false, "type " + want + " to confirm"
1717}
18
19// prefix8 returns s truncated to its first 8 characters, or s unchanged
20// if it is shorter.
21func prefix8(s string) string {
22 if len(s) > 8 {
23 return s[:8]
24 }
25 return s
26}
internal/httpd/labels.go +1 −1
@@ -44,7 +44,7 @@ func (s *Server) labelSubmit(w http.ResponseWriter, r *http.Request, u store.Use
4444 name := strings.TrimSpace(r.FormValue("name"))
4545 back := func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "labels", msg) }
4646 if name == "" {
47 s.backTo(w, r, "labels", "name the label")
47 back(w, r, "name the label")
4848 return
4949 }
5050 argv := []string{"label", "set", repo, name, "--color", strings.TrimSpace(r.FormValue("color"))}
internal/httpd/snippets.go +30 −17
@@ -178,14 +178,12 @@ func (s *Server) snippetNewSubmit(w http.ResponseWriter, r *http.Request, u stor
178178 http.Redirect(w, r, "/"+u.Username+"/-/snippets/"+out.ID, http.StatusSeeOther)
179179}
180180
181// snippetAction runs a write on the snippet in the URL and returns to its
182// page with the message, or to dest (the list, for a delete) on success.
183// A snippet the viewer may not read is the 404 page, as on every read.
184func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, argv []string, stdin string, dest string) {
185 sn, _, ok := s.snippetScope(w, r)
186 if !ok {
187 return
188 }
181// snippetAction runs a write on an already-resolved snippet and returns to
182// its page with the message, or to dest (the list, for a delete) on
183// success. Callers resolve the snippet with snippetScope first, so a
184// snippet the viewer may not read is the 404 page before any confirmation
185// or write is considered.
186func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.User, sn store.Snippet, argv []string, stdin string, dest string) {
189187 page := "/" + sn.OwnerName + "/-/snippets/" + sn.PublicID
190188 if dest == "" {
191189 dest = page
@@ -207,35 +205,50 @@ func (s *Server) snippetAction(w http.ResponseWriter, r *http.Request, u store.U
207205}
208206
209207func (s *Server) snippetEditSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
210 s.snippetAction(w, r, u, []string{"snippet", "edit", r.PathValue("id"),
208 sn, _, ok := s.snippetScope(w, r)
209 if !ok {
210 return
211 }
212 s.snippetAction(w, r, u, sn, []string{"snippet", "edit", r.PathValue("id"),
211213 "--description", strings.TrimSpace(r.FormValue("description")),
212214 "--visibility", r.FormValue("visibility")}, "", "")
213215}
214216
215217func (s *Server) snippetDeleteSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
216 id := r.PathValue("id")
217 if ok, msg := confirmed(r, id); !ok {
218 sn, _, ok := s.snippetScope(w, r)
219 if !ok {
220 return
221 }
222 if ok, msg := confirmed(r, sn.PublicID); !ok {
218223 s.setFlash(w, msg)
219 http.Redirect(w, r, "/"+r.PathValue("owner")+"/-/snippets/"+id, http.StatusSeeOther)
224 http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther)
220225 return
221226 }
222 s.snippetAction(w, r, u, []string{"snippet", "delete", id}, "",
223 "/"+r.PathValue("owner")+"/-/snippets")
227 s.snippetAction(w, r, u, sn, []string{"snippet", "delete", sn.PublicID}, "",
228 "/"+sn.OwnerName+"/-/snippets")
224229}
225230
226231// An empty textarea reaches the command as empty stdin, which it refuses;
227232// the message lands on the page like any other.
228233func (s *Server) snippetFileSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
229 s.snippetAction(w, r, u, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))},
234 sn, _, ok := s.snippetScope(w, r)
235 if !ok {
236 return
237 }
238 s.snippetAction(w, r, u, sn, []string{"snippet", "file", "set", r.PathValue("id"), strings.TrimSpace(r.FormValue("name"))},
230239 r.FormValue("content"), "")
231240}
232241
233242func (s *Server) snippetFileRemoveSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
243 sn, _, ok := s.snippetScope(w, r)
244 if !ok {
245 return
246 }
234247 name := strings.TrimSpace(r.FormValue("name"))
235248 if ok, msg := confirmed(r, name); !ok {
236249 s.setFlash(w, msg)
237 http.Redirect(w, r, "/"+r.PathValue("owner")+"/-/snippets/"+r.PathValue("id"), http.StatusSeeOther)
250 http.Redirect(w, r, "/"+sn.OwnerName+"/-/snippets/"+sn.PublicID, http.StatusSeeOther)
238251 return
239252 }
240 s.snippetAction(w, r, u, []string{"snippet", "file", "remove", r.PathValue("id"), name}, "", "")
253 s.snippetAction(w, r, u, sn, []string{"snippet", "file", "remove", r.PathValue("id"), name}, "", "")
241254}