home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fixed critical login TOTP bypass bug! whoops!!!!!

+163 -96
+1 -18
admin/accounthttp.go
··· 134 134 return 135 135 } 136 136 137 - if !r.Form.Has("password") || !r.Form.Has("totp") { 137 + if !r.Form.Has("password") { 138 138 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 139 139 return 140 140 } ··· 151 151 controller.SetSessionError(app.DB, session, "Incorrect password.") 152 152 http.Redirect(w, r, "/admin/account", http.StatusFound) 153 153 return 154 - } 155 - 156 - totpMethod, err := controller.CheckTOTPForAccount(app.DB, session.Account.ID, r.Form.Get("totp")) 157 - if err != nil { 158 - fmt.Fprintf(os.Stderr, "Failed to fetch account: %v\n", err) 159 - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 160 - http.Redirect(w, r, "/admin/account", http.StatusFound) 161 - return 162 - } 163 - if totpMethod == nil { 164 - fmt.Printf( 165 - "[%s] WARN: Account \"%s\" attempted account deletion with incorrect TOTP.\n", 166 - time.Now().Format(time.UnixDate), 167 - session.Account.Username, 168 - ) 169 - controller.SetSessionError(app.DB, session, "Incorrect TOTP.") 170 - http.Redirect(w, r, "/admin/account", http.StatusFound) 171 154 } 172 155 173 156 err = controller.DeleteAccount(app.DB, session.Account.ID)
+125 -70
admin/http.go
··· 31 31 })) 32 32 33 33 mux.Handle("/login", loginHandler(app)) 34 - mux.Handle("/logout", requireAccount(app, logoutHandler(app))) 34 + mux.Handle("/totp", loginTOTPHandler(app)) 35 + mux.Handle("/logout", requireAccount(logoutHandler(app))) 35 36 36 37 mux.Handle("/register", registerAccountHandler(app)) 37 38 38 - mux.Handle("/account", requireAccount(app, accountIndexHandler(app))) 39 - mux.Handle("/account/", requireAccount(app, http.StripPrefix("/account", accountHandler(app)))) 39 + mux.Handle("/account", requireAccount(accountIndexHandler(app))) 40 + mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app)))) 40 41 41 - mux.Handle("/release/", requireAccount(app, http.StripPrefix("/release", serveRelease(app)))) 42 - mux.Handle("/artist/", requireAccount(app, http.StripPrefix("/artist", serveArtist(app)))) 43 - mux.Handle("/track/", requireAccount(app, http.StripPrefix("/track", serveTrack(app)))) 42 + mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app)))) 43 + mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app)))) 44 + mux.Handle("/track/", requireAccount(http.StripPrefix("/track", serveTrack(app)))) 44 45 45 46 mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) 46 47 47 - mux.Handle("/", requireAccount(app, AdminIndexHandler(app))) 48 + mux.Handle("/", requireAccount(AdminIndexHandler(app))) 48 49 49 50 // response wrapper to make sure a session cookie exists 50 51 return enforceSession(app, mux) ··· 243 244 http.Redirect(w, r, "/admin", http.StatusFound) 244 245 return 245 246 } 246 - 247 247 render() 248 248 return 249 249 } ··· 254 254 return 255 255 } 256 256 257 - type LoginRequest struct { 258 - Username string `json:"username"` 259 - Password string `json:"password"` 260 - TOTP string `json:"totp"` 257 + if !r.Form.Has("username") || !r.Form.Has("password") { 258 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 259 + return 261 260 } 262 - credentials := LoginRequest{ 263 - Username: r.Form.Get("username"), 264 - Password: r.Form.Get("password"), 265 - TOTP: r.Form.Get("totp"), 266 - } 261 + 262 + username := r.FormValue("username") 263 + password := r.FormValue("password") 267 264 268 - account, err := controller.GetAccountByUsername(app.DB, credentials.Username) 265 + account, err := controller.GetAccountByUsername(app.DB, username) 269 266 if err != nil { 270 267 fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account for login: %v\n", err) 271 268 controller.SetSessionError(app.DB, session, "Invalid username or password.") ··· 278 275 return 279 276 } 280 277 281 - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) 278 + err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) 282 279 if err != nil { 283 280 fmt.Printf( 284 281 "[%s] INFO: Account \"%s\" attempted login with incorrect password.\n", ··· 290 287 return 291 288 } 292 289 293 - var totpMethod *model.TOTP 294 - if len(credentials.TOTP) == 0 { 295 - // check if user has TOTP 296 - totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) 290 + totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) 291 + if err != nil { 292 + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) 293 + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 294 + render() 295 + return 296 + } 297 + 298 + if len(totps) > 0 { 299 + err = controller.SetSessionAttemptAccount(app.DB, session, account) 297 300 if err != nil { 298 - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) 301 + fmt.Fprintf(os.Stderr, "WARN: Failed to set attempt session: %v\n", err) 299 302 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 300 303 render() 301 304 return 302 305 } 306 + http.Redirect(w, r, "/admin/totp", http.StatusFound) 307 + return 308 + } 303 309 304 - if len(totps) > 0 { 305 - type loginTOTPData struct { 306 - Session *model.Session 307 - Username string 308 - Password string 309 - } 310 - err = loginTOTPTemplate.Execute(w, loginTOTPData{ 311 - Session: session, 312 - Username: credentials.Username, 313 - Password: credentials.Password, 314 - }) 315 - if err != nil { 316 - fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) 317 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 318 - return 319 - } 320 - } 321 - } else { 322 - totpMethod, err = controller.CheckTOTPForAccount(app.DB, account.ID, credentials.TOTP) 310 + fmt.Printf( 311 + "[%s] INFO: Account \"%s\" logged in\n", 312 + time.Now().Format(time.UnixDate), 313 + account.Username, 314 + ) 315 + 316 + // TODO: log login activity to user 317 + 318 + // login success! 319 + err = controller.SetSessionAccount(app.DB, session, account) 320 + if err != nil { 321 + fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) 322 + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 323 + render() 324 + return 325 + } 326 + controller.SetSessionMessage(app.DB, session, "") 327 + controller.SetSessionError(app.DB, session, "") 328 + http.Redirect(w, r, "/admin", http.StatusFound) 329 + }) 330 + } 331 + 332 + func loginTOTPHandler(app *model.AppState) http.Handler { 333 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 334 + session := r.Context().Value("session").(*model.Session) 335 + 336 + if session.AttemptAccount == nil { 337 + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) 338 + return 339 + } 340 + 341 + type loginTOTPData struct { 342 + Session *model.Session 343 + } 344 + 345 + render := func() { 346 + err := loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) 323 347 if err != nil { 324 - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) 325 - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 326 - render() 348 + fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) 349 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 327 350 return 328 351 } 329 - if totpMethod == nil { 330 - controller.SetSessionError(app.DB, session, "Invalid TOTP.") 331 - render() 332 - return 333 - } 352 + } 353 + 354 + if r.Method == http.MethodGet { 355 + render() 356 + return 357 + } 358 + 359 + if r.Method != http.MethodPost { 360 + http.NotFound(w, r) 361 + return 334 362 } 335 363 336 - if totpMethod != nil { 337 - fmt.Printf( 338 - "[%s] INFO: Account \"%s\" logged in with method \"%s\"\n", 339 - time.Now().Format(time.UnixDate), 340 - account.Username, 341 - totpMethod.Name, 342 - ) 343 - } else { 344 - fmt.Printf( 345 - "[%s] INFO: Account \"%s\" logged in\n", 346 - time.Now().Format(time.UnixDate), 347 - account.Username, 348 - ) 364 + r.ParseForm() 365 + 366 + if !r.Form.Has("totp") { 367 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 368 + return 369 + } 370 + 371 + totpCode := r.FormValue("totp") 372 + 373 + if len(totpCode) != controller.TOTP_CODE_LENGTH { 374 + controller.SetSessionError(app.DB, session, "Invalid TOTP.") 375 + render() 376 + return 377 + } 378 + 379 + totpMethod, err := controller.CheckTOTPForAccount(app.DB, session.AttemptAccount.ID, totpCode) 380 + if err != nil { 381 + fmt.Fprintf(os.Stderr, "WARN: Failed to check TOTPs: %v\n", err) 382 + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 383 + render() 384 + return 385 + } 386 + if totpMethod == nil { 387 + controller.SetSessionError(app.DB, session, "Invalid TOTP.") 388 + render() 389 + return 349 390 } 350 391 351 - // TODO: log login activity to user 392 + fmt.Printf( 393 + "[%s] INFO: Account \"%s\" logged in with method \"%s\"\n", 394 + time.Now().Format(time.UnixDate), 395 + session.AttemptAccount.Username, 396 + totpMethod.Name, 397 + ) 352 398 353 - // login success! 354 - controller.SetSessionAccount(app.DB, session, account) 399 + err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount) 400 + if err != nil { 401 + fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) 402 + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 403 + render() 404 + return 405 + } 406 + err = controller.SetSessionAttemptAccount(app.DB, session, nil) 407 + if err != nil { 408 + fmt.Fprintf(os.Stderr, "WARN: Failed to clear attempt session: %v\n", err) 409 + } 355 410 controller.SetSessionMessage(app.DB, session, "") 356 411 controller.SetSessionError(app.DB, session, "") 357 412 http.Redirect(w, r, "/admin", http.StatusFound) ··· 387 442 }) 388 443 } 389 444 390 - func requireAccount(app *model.AppState, next http.Handler) http.HandlerFunc { 445 + func requireAccount(next http.Handler) http.HandlerFunc { 391 446 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 392 447 session := r.Context().Value("session").(*model.Session) 393 448 if session.Account == nil { ··· 425 480 sessionCookie, err := r.Cookie(model.COOKIE_TOKEN) 426 481 if err != nil && err != http.ErrNoCookie { 427 482 fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session cookie: %v\n", err) 428 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 483 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 429 484 return 430 485 } 431 486 432 487 var session *model.Session 433 - 488 + 434 489 if sessionCookie != nil { 435 490 // fetch existing session 436 491 session, err = controller.GetSession(app.DB, sessionCookie.Value)
+8 -3
admin/views/login-totp.html
··· 26 26 27 27 {{define "content"}} 28 28 <main> 29 - <form action="/admin/login" method="POST" id="login-totp"> 29 + {{if .Session.Message.Valid}} 30 + <p id="message">{{html .Session.Message.String}}</p> 31 + {{end}} 32 + {{if .Session.Error.Valid}} 33 + <p id="error">{{html .Session.Error.String}}</p> 34 + {{end}} 35 + 36 + <form action="/admin/totp" method="POST" id="login-totp"> 30 37 <h1>Two-Factor Authentication</h1> 31 38 32 39 <div> 33 40 <label for="totp">TOTP</label> 34 41 <input type="text" name="totp" value="" autocomplete="one-time-code" required autofocus> 35 - <input type="hidden" name="username" value="{{.Username}}"> 36 - <input type="hidden" name="password" value="{{.Password}}"> 37 42 </div> 38 43 39 44 <button type="submit" class="save">Login</button>
+20 -1
controller/session.go
··· 49 49 // return err 50 50 // } 51 51 52 + func SetSessionAttemptAccount(db *sqlx.DB, session *model.Session, account *model.Account) error { 53 + var err error 54 + session.AttemptAccount = account 55 + if account == nil { 56 + _, err = db.Exec("UPDATE session SET attempt_account=NULL WHERE token=$1", session.Token) 57 + } else { 58 + _, err = db.Exec("UPDATE session SET attempt_account=$2 WHERE token=$1", session.Token, account.ID) 59 + } 60 + return err 61 + } 62 + 52 63 func SetSessionAccount(db *sqlx.DB, session *model.Session, account *model.Account) error { 53 64 var err error 54 65 session.Account = account ··· 89 100 func GetSession(db *sqlx.DB, token string) (*model.Session, error) { 90 101 type dbSession struct { 91 102 model.Session 92 - AccountID sql.NullString `db:"account"` 103 + AttemptAccountID sql.NullString `db:"attempt_account"` 104 + AccountID sql.NullString `db:"account"` 93 105 } 94 106 95 107 session := dbSession{} ··· 104 116 105 117 if session.AccountID.Valid { 106 118 session.Account, err = GetAccountByID(db, session.AccountID.String) 119 + if err != nil { 120 + return nil, err 121 + } 122 + } 123 + 124 + if session.AttemptAccountID.Valid { 125 + session.AttemptAccount, err = GetAccountByID(db, session.AttemptAccountID.String) 107 126 if err != nil { 108 127 return nil, err 109 128 }
+4 -3
model/session.go
··· 6 6 ) 7 7 8 8 type Session struct { 9 - Token string `json:"token" db:"token"` 9 + Token string `json:"-" db:"token"` 10 10 UserAgent string `json:"user_agent" db:"user_agent"` 11 11 CreatedAt time.Time `json:"created_at" db:"created_at"` 12 - ExpiresAt time.Time `json:"expires_at" db:"expires_at"` 12 + ExpiresAt time.Time `json:"-" db:"expires_at"` 13 13 14 - Account *Account `json:"-" db:"account"` 14 + Account *Account `json:"-" db:"-"` 15 + AttemptAccount *Account `json:"-" db:"-"` 15 16 Message sql.NullString `json:"-" db:"message"` 16 17 Error sql.NullString `json:"-" db:"error"` 17 18 }
+2
schema-migration/000-init.sql
··· 35 35 created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, 36 36 expires_at TIMESTAMP DEFAULT NULL, 37 37 account UUID, 38 + attempt_account UUID, 38 39 message TEXT, 39 40 error TEXT 40 41 ); ··· 120 121 121 122 ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 122 123 ALTER TABLE arimelody.session ADD CONSTRAINT session_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 124 + ALTER TABLE arimelody.session ADD CONSTRAINT session_attempt_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 123 125 ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 124 126 125 127 ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE;
+3 -1
schema-migration/001-pre-versioning.sql
··· 35 35 created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, 36 36 expires_at TIMESTAMP DEFAULT NULL, 37 37 account UUID, 38 + attempt_account UUID, 38 39 message TEXT, 39 40 error TEXT 40 41 ); ··· 52 53 53 54 -- Foreign keys 54 55 ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 55 - ALTER TABLE arimelody.session ADD CONSTRAINT session FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 56 + ALTER TABLE arimelody.session ADD CONSTRAINT session_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 57 + ALTER TABLE arimelody.session ADD CONSTRAINT session_attempt_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 56 58 ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;