Bezbednosne i integritetne popravke iz pregleda koda
- foreign_keys pragma po konekciji (DSN _pragma) — ranije aktivna na samo delu konekcija iz pula, rizik siročadi u bazi pri brisanju - busy_timeout + _txlock=immediate za konkurentan pristup SQLite bazi - ispravljen DnevniPrometMaloprodaje (DATE() -> substr) — dnevni pazar za KIR/KPO je bio uvek nula - rate-limit (isti IP-bruteforce brojač) dodat na TOTP/2FA verifikaciju - ispravljen nil-pointer u DodajDeloNalogu + uklonjeni debug logovi - izvuciIP prihvata X-Real-IP/X-Forwarded-For samo sa poverljive (loopback/privatne) adrese — sprečava zaobilaženje bruteforce brave - ispravljen naziv dozvole podesavanja.izmena -> podesavanja.izmeni - brisanje artikala/kategorija prebačeno sa GET na POST (CSRF zaštita)
This commit is contained in:
@@ -1090,7 +1090,7 @@ func (h *Handler) BeStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// BeResetAudit šalje reset_audit komandu kartica emulatoru i vraća HTMX fragment.
|
||||
func (h *Handler) BeResetAudit(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmena"); !ok {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
bePort := os.Getenv("BE_PORT")
|
||||
|
||||
+50
-11
@@ -186,6 +186,23 @@ func (h *Handler) VerifikujTotp(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// isti bruteforce brojač kao za lozinku — deli IP-bazirano zaključavanje
|
||||
ip := izvuciIP(r)
|
||||
od := time.Now().Add(-prozorPrijave)
|
||||
n, _ := h.PokusajiRepo.BrojNeuspeha(r.Context(), ip, od)
|
||||
if n >= maxNeuspehaPrijave {
|
||||
if preostalo, zaklj := h.preostaloBruteforce(r.Context(), ip, od); zaklj {
|
||||
auth.LogZaklucano(ip, korisnik.KorisnickoIme)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), &korisnik.ID, ip, r.UserAgent(), "ip_zaklucano_totp", false)
|
||||
h.renderujStandalone(w, "totp_provera", map[string]any{
|
||||
"Greska": "zakljucano",
|
||||
"Preostalo": preostalo,
|
||||
"CsrfToken": middleware.CsrfToken(r.Context()),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
kod := r.FormValue("kod")
|
||||
validan := auth.VerifikujTotpKod(kod, korisnik.TotpTajna)
|
||||
if !validan {
|
||||
@@ -196,10 +213,15 @@ func (h *Handler) VerifikujTotp(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if !validan {
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnik.KorisnickoIme, false)
|
||||
_ = h.LoginIstorijsaRepo.Zabeleži(r.Context(), &korisnik.ID, ip, r.UserAgent(), "pogrešan_totp", false)
|
||||
auth.LogNeuspehPrijave(ip, korisnik.KorisnickoIme, "wrong_totp")
|
||||
http.Redirect(w, r, "/prijava/totp?greska=1", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
_ = h.PokusajiRepo.Zabeleži(r.Context(), ip, korisnik.KorisnickoIme, true)
|
||||
|
||||
novoIstice := time.Now().Add(trajanjeSeije)
|
||||
if err := h.SesijeRepo.PotvrdiTotp(r.Context(), kolacic.Value, novoIstice); err != nil {
|
||||
http.Redirect(w, r, "/prijava?greska=2", http.StatusSeeOther)
|
||||
@@ -295,24 +317,41 @@ func (h *Handler) preostaloBruteforce(ctx context.Context, ip string, od time.Ti
|
||||
return fmt.Sprintf("%d sek", sek), true
|
||||
}
|
||||
|
||||
// izvuciIP čita pravi IP klijenta — najpre X-Real-IP (koji nginx postavlja),
|
||||
// zatim poslednji X-Forwarded-For (dodat od strane proxy-a), pa RemoteAddr
|
||||
// izvuciIP čita pravi IP klijenta iz X-Real-IP/X-Forwarded-For, ALI samo ako
|
||||
// zahtev fizički stiže sa poverljive adrese (Caddy na istom hostu ili Docker
|
||||
// mreža — loopback/privatni opseg). Ova zaglavlja su inače potpuno pod
|
||||
// kontrolom klijenta; slepo verovanje u njih zaobilazi bruteforce zaključavanje
|
||||
// prijave/TOTP-a. Ako RemoteAddr nije poverljiv, header se ignoriše.
|
||||
func izvuciIP(r *http.Request) string {
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
||||
// uzimamo poslednju vrednost — nju dodaje naš proxy, ne klijent
|
||||
parts := strings.Split(fwd, ",")
|
||||
return strings.TrimSpace(parts[len(parts)-1])
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
|
||||
if remoteAdresaJePoverljiva(host) {
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
|
||||
// uzimamo poslednju vrednost — nju dodaje naš proxy, ne klijent
|
||||
parts := strings.Split(fwd, ",")
|
||||
return strings.TrimSpace(parts[len(parts)-1])
|
||||
}
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// remoteAdresaJePoverljiva vraća true ako je adresa loopback ili iz privatnog
|
||||
// (RFC 1918) opsega — tipično za reverse proxy na istom hostu ili u istoj Docker mreži.
|
||||
func remoteAdresaJePoverljiva(host string) bool {
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.IsLoopback() || ip.IsPrivate()
|
||||
}
|
||||
|
||||
func napraviKolacic(token string, istice time.Time) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: imeKolacica,
|
||||
|
||||
@@ -1049,12 +1049,15 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// predlog ako status nije „Primljeno" (forma više ne određuje — server odlučuje)
|
||||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), nalogID)
|
||||
predlozeno := nalog != nil && nalog.Status != model.StatusPrimljeno
|
||||
slog.Info("DODAJ_DEO_IN", "status", nalog.Status, "predlozeno", predlozeno, "kol", kolicina)
|
||||
nalog, err := h.ServisRepo.DohvatiID(r.Context(), nalogID)
|
||||
if err != nil || nalog == nil {
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Nalog nije pronađen.")
|
||||
http.Redirect(w, r, "/servis", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
predlozeno := nalog.Status != model.StatusPrimljeno
|
||||
// atomično: ugradi ono što imamo (skida sa lagera, ne ide u minus), višak u potraživane
|
||||
ugradjeno, nedostaje, err := h.ServisniDeloviRepo.UgradiIliPotrazuj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID, predlozeno)
|
||||
slog.Info("DODAJ_DEO_OUT", "ugradjeno", ugradjeno, "nedostaje", nedostaje, "err", err)
|
||||
if err != nil {
|
||||
slog.Error("greška pri dodavanju dela", "error", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||||
|
||||
@@ -13,8 +13,10 @@ func TestIzvuciIP(t *testing.T) {
|
||||
remoteAddr string
|
||||
ocek string
|
||||
}{
|
||||
{"X-Real-IP ima prioritet", "1.2.3.4", "9.9.9.9", "5.5.5.5:1234", "1.2.3.4"},
|
||||
{"poslednji X-Forwarded-For", "", "1.1.1.1, 2.2.2.2, 3.3.3.3", "5.5.5.5:1234", "3.3.3.3"},
|
||||
{"X-Real-IP ima prioritet kad je proxy poverljiv (loopback)", "1.2.3.4", "9.9.9.9", "127.0.0.1:1234", "1.2.3.4"},
|
||||
{"X-Real-IP ima prioritet kad je proxy poverljiv (privatna Docker mreža)", "1.2.3.4", "9.9.9.9", "172.18.0.1:1234", "1.2.3.4"},
|
||||
{"poslednji X-Forwarded-For kad je proxy poverljiv", "", "1.1.1.1, 2.2.2.2, 3.3.3.3", "127.0.0.1:1234", "3.3.3.3"},
|
||||
{"zaglavlje se IGNORIŠE kad RemoteAddr nije poverljiv (javna adresa — spoofing)", "1.2.3.4", "", "5.5.5.5:1234", "5.5.5.5"},
|
||||
{"RemoteAddr bez porta kad nema zaglavlja", "", "", "5.5.5.5:1234", "5.5.5.5"},
|
||||
{"RemoteAddr kakav jeste ako nije host:port", "", "", "neispravan", "neispravan"},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user