Bezbednost: limit veličine upload tela, anti-enumeracija pri prijavi, strings.Cut
- CSRF middleware postavlja MaxBytesReader (6 MB) za multipart pre parsiranja — pojedinačni upload handleri nisu mogli da ograniče veličinu jer čitanje _csrf polja već parsira celo telo - prijava: dummy bcrypt poređenje kada korisnik ne postoji, da vreme odgovora bude isto kao kod postojećeg korisnika (sprečava enumeraciju imena) - podesavanja: strings.Split(...)[0] zamenjen sa strings.Cut
This commit is contained in:
@@ -27,6 +27,17 @@ func ProveriLozinku(hash, lozinka string) bool {
|
|||||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(lozinka)) == nil
|
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
|
// GenerisiToken generiše nasumičan UUID token za sesiju
|
||||||
func GenerisiToken() string {
|
func GenerisiToken() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ func (h *Handler) OtpremiLoginPozadinu(w http.ResponseWriter, r *http.Request) {
|
|||||||
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
staraPodesavanja, _ := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
if stara := staraPodesavanja["login_pozadina"]; stara != "" {
|
if stara := staraPodesavanja["login_pozadina"]; stara != "" {
|
||||||
// putanja u bazi je oblika /static/uploads/ime.ext?v=..., izvlačimo samo ime fajla
|
// 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)
|
staroIme := filepath.Base(deoBezverzije)
|
||||||
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
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)
|
podesavanja, err := ntechsqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if stara := podesavanja["login_pozadina"]; stara != "" {
|
if stara := podesavanja["login_pozadina"]; stara != "" {
|
||||||
deoBezverzije := strings.Split(stara, "?")[0]
|
deoBezverzije, _, _ := strings.Cut(stara, "?")
|
||||||
staroIme := filepath.Base(deoBezverzije)
|
staroIme := filepath.Base(deoBezverzije)
|
||||||
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
os.Remove(filepath.Join("web/static/uploads", staroIme))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ func (h *Handler) Prijava(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
korisnik, err := h.KorisniciRepo.DohvatiPoImenu(r.Context(), korisnickoIme)
|
||||||
if err != nil {
|
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.PokusajiRepo.Zabeleži(r.Context(), ip, korisnickoIme, false)
|
||||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "korisnik_ne_postoji", false)
|
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), nil, ip, r.UserAgent(), "korisnik_ne_postoji", false)
|
||||||
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
auth.LogNeuspehPrijave(ip, korisnickoIme, "wrong_user")
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const csrfKolacic = "ntech_csrf"
|
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{}
|
type csrfKljucTip struct{}
|
||||||
|
|
||||||
var csrfKljuc = csrfKljucTip{}
|
var csrfKljuc = csrfKljucTip{}
|
||||||
@@ -52,6 +59,11 @@ func CsrfMiddleware(next http.Handler) http.Handler {
|
|||||||
// validiramo na svim mutabilnim HTTP metodama
|
// validiramo na svim mutabilnim HTTP metodama
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
|
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)
|
// čitamo token iz tela forme ili zaglavlja (za AJAX)
|
||||||
submitted := r.FormValue("_csrf")
|
submitted := r.FormValue("_csrf")
|
||||||
if submitted == "" {
|
if submitted == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user