Diagnozė ir taisymas — dev išimtis + žetonas
Diagnozė. „referer not supplied" — tai kalba Origin/Referer apsaugos pusė, ne žetono pusė. gorilla/csrf daro prielaidą apie produkcijos HTTPS, tad kiekvienai nesaugiai užklausai reikalauja to paties saugaus šaltinio įrodymo: galiojančio Origin ar Referer antraštės per TLS. Tavo dev serveris — paprastas http://localhost, kuris tokio įrodymo neduoda, tad patikra atmeta užklausą dar prieš pažvelgdama į žetoną. Štai kodėl vien žetono lauko pridėjimas nebūtų padėjęs — pirma reikia praeiti pro šaltinio patikrą.
Tad taisymas — du ėjimai: (a) pasakyk middleware „ši užklausa tikrai yra plaintext HTTP" tik dev, ir (b) duok formoms žetoną, kad ir antra pusė praeitų.
Penki failai kaip skirtukai: router.go → nav.go → auth.go → auth.templ → layout.templ.
1. internal/handlers/router.go (modify) — tik-dev išimtis, pridėta prieš csrf.Protect:
if !h.Cfg.IsProd() {
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
next.ServeHTTP(w, csrf.PlaintextHTTPRequest(req))
})
})
}
PlaintextHTTPRequest atpalaiduoja tik Origin/Referer patikrą ir tik kai !IsProd(). Produkcija (HTTPS Railway krašte) šito niekada nevykdo, tad jos griežta šaltinio apsauga lieka pilnai apšarvuota. Žetono patikra abiem atvejais nepaliesta.
2. internal/web/nav.go (modify) — Nav gauna CSRFToken lauką.
3. internal/handlers/auth.go (modify) — nav() jį užpildo: web.Nav{CSRFToken: csrf.Token(r)}. (csrf.Token perskaito užklausos žetoną, kurį nustatė middleware.)
4. internal/web/auth.templ (modify) — authCSRF pagalbininkas atvaizduoja paslėptą lauką, ir kiekviena forma jį kviečia:
templ authCSRF(token string) {
<input type="hidden" name="gorilla.csrf.Token" value={ token }/>
}
// kiekvienos <form> viduje: @authCSRF(nav.CSRFToken)
5. internal/web/layout.templ (modify) — atsijungimo forma antraštėje gauna tą patį paslėptą lauką.
Patikrink — dabar veikia. templ generate && go run ./cmd/signflow. Prisijunk: pasieki skydelį. Registruokis, atsijunk — viskas gerai. Peržiūrėk /login šaltinį ir pamatysi žetoną:
<input type="hidden" name="gorilla.csrf.Token" value="Ck3p...ilga-atsitiktinė...">
Ir klastotė vis dar nepavyksta: POST be lauko atmetamas net dev aplinkoje (žetono pusė visada įjungta):
$ curl -si -X POST localhost:8080/login -d 'email=a@b.co&password=x' | head -1
HTTP/1.1 403 Forbidden ← nėra žetono → vis tiek blokuota. Būtent taip.
HTMX ir
X-CSRF-Tokenantraštė. Vėliau įkėlimai POST'ins per HTMX su multipart kūnu. Perskaityti tą kūną vien tam, kad rastum paslėptą lauką, būtų švaistymas — tadgorilla/csrftaip pat priima žetonąX-CSRF-Tokenantraštėje. Įdėk žetoną į<meta>žymę ir liepk HTMX siųsti jį su kiekviena užklausa:<meta name="csrf-token" content={ nav.CSRFToken }/> <body hx-headers='{"X-CSRF-Token": "…"}'>Tas pats žetonas, siunčiamas antraštėje, o ne kūne — multipart įkėlimas patvirtinamas be jokio pakartotinio analizavimo. Tai naudosi dokumentų fazėje.
Pamoka buvo ne taisymas — o 403. Dabar žinai, kad CSRF apsauga — dvi patikros: įrodyk to paties šaltinio ketinimą (Origin/Referer) ir atkartok slaptą žetoną. Žetonas — pusė, kurią visi prisimena; šaltinio patikra — pusė, kuri tave 403'ino. Abi turi praeiti.