Bezbednost: SSRF — proširena whitelist na privatne mreže (192.168.x, 10.x, 172.16-31.x)

Pre: samo 127.0.0.1 i localhost — nije radilo sa Fisk serverom na drugoj mašini u LAN-u
Posle: jePrivatnaAdresa() dozvoljava sve privatne IPv4 opsege + localhost
Hostname se koristi direktno (iz validiranog URL-a), port kao integer
This commit is contained in:
2026-06-25 22:59:17 +02:00
parent edcea0ddcf
commit 7b62217382
+27 -6
View File
@@ -7,6 +7,7 @@ import (
"html"
"io"
"log/slog"
"net"
"net/http"
"net/url"
"os"
@@ -924,12 +925,12 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
pfrURL = vrednostIliDefault(podesavanja, "pfr_url", "http://127.0.0.1:4566")
}
// SSRF zaštita: URL validiramo, izvlačimo SAMO port, a host hardkodiramo.
// Port konvertujemo u integer da presečemo CodeQL taint lanac — broj ne može
// da sadrži URL manipulaciju, pa rekonstruisani URL nije korisnički unos.
// SSRF zaštita: dozvoljeni su samo localhost i privatne mreže (192.168.x.x,
// 10.x.x.x, 172.16-31.x.x, 127.x.x.x). Port izvlačimo kao integer da presečemo
// CodeQL taint lanac — broj ne može da sadrži URL manipulaciju.
parsedURL, err := url.Parse(pfrURL)
if err != nil || (parsedURL.Hostname() != "127.0.0.1" && parsedURL.Hostname() != "localhost") {
http.Error(w, "Nevažeći PFR URL — dozvoljen samo localhost", http.StatusBadRequest)
if err != nil || !jePrivatnaAdresa(parsedURL.Hostname()) {
http.Error(w, "Nevažeći PFR URL — dozvoljeni su samo lokalni/privatni hostovi (127.x, 192.168.x, 10.x, 172.16-31.x)", http.StatusBadRequest)
return
}
port := 4566
@@ -938,7 +939,7 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
port = n
}
}
statusURL := fmt.Sprintf("http://127.0.0.1:%d/api/status", port)
statusURL := fmt.Sprintf("http://%s:%d/api/status", parsedURL.Hostname(), port)
klijent := &http.Client{Timeout: 5 * time.Second}
resp, err := klijent.Get(statusURL)
@@ -967,6 +968,26 @@ func (h *Handler) TestFiskalizacije(w http.ResponseWriter, r *http.Request) {
</div>`, html.EscapeString(pfrURL), html.EscapeString(tin), html.EscapeString(lastInvoice), html.EscapeString(sdcDateTime))
}
// jePrivatnaAdresa proverava da li je hostname u opsegu privatnih/lokalnih mreža.
// Dozvoljeni: 127.x.x.x, localhost, 10.x.x.x, 172.16-31.x.x, 192.168.x.x.
func jePrivatnaAdresa(hostname string) bool {
if hostname == "localhost" {
return true
}
ip := net.ParseIP(hostname)
if ip == nil {
return false
}
ip4 := ip.To4()
if ip4 == nil {
return false
}
return ip4[0] == 127 || // loopback
ip4[0] == 10 || // privatna klasa A
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // privatna klasa B
(ip4[0] == 192 && ip4[1] == 168) // privatna klasa C
}
// PodesavanjaSistem renderuje stranicu sa sistemskim podešavanjima (backup)
func (h *Handler) PodesavanjaSistem(w http.ResponseWriter, r *http.Request) {
if _, ok := h.zahtevajDozvolu(w, r, "podesavanja.pregled"); !ok {