Commit d8dfc35992
d8dfc35992ffb900c7e130898f2c5fd12b771340
parent: dbed1606f9
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-20 11:10 UTC
web: mask a short device token on the settings page
prefix8 returns anything under nine characters unchanged, and device
add enforces no minimum token length, so a short token was rendered
whole. shortToken already handled this in the control layer two
commits before the page was written; export it and render the column
through it, so the two surfaces cannot drift again. prefix8 itself is
unchanged: it also derives the typed-confirmation strings for key,
PGP and email removal.
Ref #89
internal/httpd/account.go
+9 −7
| @@ -32,14 +32,16 @@ type accountPGP struct { |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | // accountDevice is one registered APNs device as the settings page shows |
| 35 | | // it. Token is truncated to its first 8 characters: the full token is |
| 36 | | // device-identifying and never reaches the page. |
| 35 | // it. The full token is device-identifying and never reaches the page. |
| 37 | 36 | type accountDevice struct { |
| 38 | | ID int64 |
| 39 | | Label string |
| 37 | ID int64 |
| 38 | Label string |
| 39 | // Token is rendered by control.ShortToken, the same renderer |
| 40 | // notifications device list uses: prefix8 returns anything under nine |
| 41 | // characters unchanged, and device add enforces no minimum length. |
| 40 | 42 | Token string |
| 41 | 43 | LastSeenAt string |
| 42 | | Confirm string // the truncated token, typed back to confirm removal |
| 44 | Confirm string // the token's first 8 characters, typed back to confirm removal |
| 43 | 45 | } |
| 44 | 46 | |
| 45 | 47 | // accountForm renders the account's own settings: keys, addresses, and the |
| @@ -81,8 +83,8 @@ func (s *Server) accountPage(w http.ResponseWriter, r *http.Request, u store.Use |
| 81 | 83 | var devices []accountDevice |
| 82 | 84 | if list, err := s.st.PushDevices(u.ID); err == nil { |
| 83 | 85 | for _, d := range list { |
| 84 | | token := prefix8(d.Token) |
| 85 | | devices = append(devices, accountDevice{ID: d.ID, Label: d.Label, Token: token, LastSeenAt: d.LastSeenAt, Confirm: token}) |
| 86 | devices = append(devices, accountDevice{ID: d.ID, Label: d.Label, |
| 87 | Token: control.ShortToken(d.Token), LastSeenAt: d.LastSeenAt, Confirm: prefix8(d.Token)}) |
| 86 | 88 | } |
| 87 | 89 | } |
| 88 | 90 | |
internal/httpd/account_test.go
+30
| @@ -135,3 +135,33 @@ func TestAccountSubmitDeviceRemove(t *testing.T) { |
| 135 | 135 | t.Fatalf("device not removed: %v", devices) |
| 136 | 136 | } |
| 137 | 137 | } |
| 138 | |
| 139 | // A token at or under the truncation length is masked rather than shown |
| 140 | // whole, as notifications device list masks it. prefix8 returns anything |
| 141 | // shorter than nine characters unchanged, and device add enforces no |
| 142 | // minimum length, so the short token is a value that reaches the page. |
| 143 | func TestAccountPageMasksAShortDeviceToken(t *testing.T) { |
| 144 | st, err := store.Open(":memory:") |
| 145 | if err != nil { |
| 146 | t.Fatal(err) |
| 147 | } |
| 148 | defer st.Close() |
| 149 | if err := st.MigrateUp(); err != nil { |
| 150 | t.Fatal(err) |
| 151 | } |
| 152 | uid, err := st.CreateUser("alice", false) |
| 153 | if err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | if _, err := st.AddPushDevice(uid, "abc123", "iphone"); err != nil { |
| 157 | t.Fatal(err) |
| 158 | } |
| 159 | |
| 160 | s := New(config.Default(), st) |
| 161 | rr := httptest.NewRecorder() |
| 162 | s.accountPage(rr, httptest.NewRequest("GET", "/settings", nil), store.User{ID: uid, Username: "alice"}) |
| 163 | |
| 164 | if strings.Contains(rr.Body.String(), `class="mono">abc123<`) { |
| 165 | t.Fatalf("the page printed the short token verbatim:\n%s", rr.Body.String()) |
| 166 | } |
| 167 | } |