Regression for the share secret exposure (GHSA-833g-cqhp-h72j): the share API must not serialize the bcrypt password hash or the bypass token, while still persisting them server-side so password-protected shares keep working.
(t *testing.T)
| 97 | // must not serialize the bcrypt password hash or the bypass token, while still |
| 98 | // persisting them server-side so password-protected shares keep working. |
| 99 | func TestSharePostHandlerDoesNotLeakSecrets(t *testing.T) { |
| 100 | root := t.TempDir() |
| 101 | userScope := filepath.Join(root, "user") |
| 102 | if err := os.MkdirAll(userScope, 0o755); err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | if err := os.WriteFile(filepath.Join(userScope, "file.txt"), []byte("x"), 0o600); err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | |
| 109 | key := []byte("test-signing-key") |
| 110 | perm := users.Permissions{Share: true, Download: true} |
| 111 | st := scopedUserStorage(t, userScope, perm, key) |
| 112 | signed := signToken(t, perm, key) |
| 113 | |
| 114 | body := `{"password":"ShareSecret123!","expires":"24","unit":"hours"}` |
| 115 | req, _ := http.NewRequest(http.MethodPost, "/file.txt", strings.NewReader(body)) |
| 116 | req.Header.Set("X-Auth", signed) |
| 117 | rec := httptest.NewRecorder() |
| 118 | handle(sharePostHandler, "", st, &settings.Server{Root: root}).ServeHTTP(rec, req) |
| 119 | |
| 120 | if rec.Code != http.StatusOK { |
| 121 | t.Fatalf("expected 200, got %d body=%q", rec.Code, rec.Body.String()) |
| 122 | } |
| 123 | |
| 124 | var resp map[string]any |
| 125 | if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { |
| 126 | t.Fatalf("decode: %v", err) |
| 127 | } |
| 128 | if _, ok := resp["password_hash"]; ok { |
| 129 | t.Errorf("VULNERABLE: response leaks password_hash: %s", rec.Body.String()) |
| 130 | } |
| 131 | if _, ok := resp["token"]; ok { |
| 132 | t.Errorf("VULNERABLE: response leaks token: %s", rec.Body.String()) |
| 133 | } |
| 134 | if resp["hasPassword"] != true { |
| 135 | t.Errorf("expected hasPassword=true, got %v", resp["hasPassword"]) |
| 136 | } |
| 137 | |
| 138 | // The secrets must still be persisted server-side (storm uses the JSON codec, |
| 139 | // so the storage struct's tags must keep emitting them). |
| 140 | stored, err := st.Share.GetByHash(resp["hash"].(string)) |
| 141 | if err != nil { |
| 142 | t.Fatalf("share not stored: %v", err) |
| 143 | } |
| 144 | if stored.PasswordHash == "" || stored.Token == "" { |
| 145 | t.Fatalf("server-side secrets not persisted: hash=%q token=%q", stored.PasswordHash, stored.Token) |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | func signShareTestToken(t *testing.T, id uint, username string, perm users.Permissions, key []byte) string { |
| 150 | t.Helper() |