diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 2606862..e1868c3 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -27,6 +27,17 @@ func ProveriLozinku(hash, lozinka string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(lozinka)) == nil } +// dummyHash je bcrypt heš fiksne vrednosti, izračunat jednom pri pokretanju. +// Koristi ga IzjednaciVremeProvere kada korisnik ne postoji. +var dummyHash, _ = bcrypt.GenerateFromPassword([]byte("ntech-dummy-lozinka"), bcryptCost) + +// IzjednaciVremeProvere izvršava bcrypt poređenje protiv fiksnog heša da bi vreme +// odgovora bilo isto kao kod postojećeg korisnika sa pogrešnom lozinkom — +// time se sprečava enumeracija korisničkih imena merenjem vremena odgovora. +func IzjednaciVremeProvere(lozinka string) { + _ = bcrypt.CompareHashAndPassword(dummyHash, []byte(lozinka)) +} + // GenerisiToken generiše nasumičan UUID token za sesiju func GenerisiToken() string { return uuid.New().String() diff --git a/internal/handler/podesavanja.go b/internal/handler/podesavanja.go index b220ff2..e0b65bb 100644 --- a/internal/handler/podesavanja.go +++ b/internal/handler/podesavanja.go @@ -539,7 +539,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) { staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) if stara := staraPodesavanja["login_pozadina"]; stara != "" { // putanja u bazi je oblika /static/uploads/ime.ext?v=..., izvlačimo samo ime fajla - deoBezverzije := strings.Split(stara, "?")[0] + deoBezverzije, _, _ := strings.Cut(stara, "?") staroIme := filepath.Base(deoBezverzije) os.Remove(filepath.Join("web/static/uploads", staroIme)) } @@ -590,7 +590,7 @@ func (h *Handler) UkloniLoginPozadinu(w http.ResponseWriter, r *http.Request) { podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB) if err == nil { if stara := podesavanja["login_pozadina"]; stara != "" { - deoBezverzije := strings.Split(stara, "?")[0] + deoBezverzije, _, _ := strings.Cut(stara, "?") staroIme := filepath.Base(deoBezverzije) os.Remove(filepath.Join("web/static/uploads", staroIme)) } diff --git a/internal/handler/prijava.go b/internal/handler/prijava.go index f92adf9..822dcd0 100644 --- a/internal/handler/prijava.go +++ b/internal/handler/prijava.go @@ -98,6 +98,8 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) { korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme) if err != nil { + // izjednačavamo vreme odgovora sa slučajem postojećeg korisnika (anti-enumeracija) + auth.IzjednaciVremeProvere(lozinka) _ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false) _ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "korisnik_ne_postoji", false) auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user") diff --git a/internal/middleware/csrf.go b/internal/middleware/csrf.go index 6cdfec4..d73de18 100644 --- a/internal/middleware/csrf.go +++ b/internal/middleware/csrf.go @@ -6,10 +6,17 @@ import ( "encoding/base64" "net/http" "os" + "strings" ) const csrfKolacic = "ntech_csrf" +// maxTeloUpload je gornja granica veličine tela multipart zahteva (upload fajlova). +// Postavlja se u middleware-u pre parsiranja jer čitanje _csrf polja već parsira +// telo — bez ovog limita pojedinačni handleri ne mogu da ga efektivno ograniče. +// Najveći legitiman upload je 5 MB (avatar, pozadina); ostatak je rezerva za form overhead. +const maxTeloUpload = 6 << 20 + type csrfKljucTip struct{} var csrfKljuc = csrfKljucTip{} @@ -52,6 +59,11 @@ func CsrfMiddleware(next http.Handler) http.Handler { // validiramo na svim mutabilnim HTTP metodama switch r.Method { case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: + // ograničavamo veličinu tela PRE parsiranja — čitanje _csrf polja parsira + // celo telo, pa je ovo jedino mesto gde limit za upload stvarno deluje + if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") { + r.Body = http.MaxBytesReader(w, r.Body, maxTeloUpload) + } // čitamo token iz tela forme ili zaglavlja (za AJAX) submitted := r.FormValue("_csrf") if submitted == "" {