add CSRF checking

This commit is contained in:
Brian Picciano 2021-08-29 22:15:58 -06:00
parent 5746a510fc
commit 15ae483fad
5 changed files with 100 additions and 3 deletions

View File

@ -142,6 +142,8 @@ func (a *api) handler() http.Handler {
staticHandler = httputil.NewSingleHostReverseProxy(a.params.StaticProxy) staticHandler = httputil.NewSingleHostReverseProxy(a.params.StaticProxy)
} }
staticHandler = setCSRFMiddleware(staticHandler)
// sugar // sugar
requirePow := func(h http.Handler) http.Handler { requirePow := func(h http.Handler) http.Handler {
return a.requirePowMiddleware(h) return a.requirePowMiddleware(h)
@ -163,7 +165,9 @@ func (a *api) handler() http.Handler {
apiMux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler()) apiMux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler())
apiMux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler()) apiMux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler())
apiHandler := logMiddleware(a.params.Logger, apiMux) var apiHandler http.Handler = apiMux
apiHandler = checkCSRFMiddleware(apiHandler)
apiHandler = logMiddleware(a.params.Logger, apiHandler)
apiHandler = annotateMiddleware(apiHandler) apiHandler = annotateMiddleware(apiHandler)
apiHandler = addResponseHeaders(map[string]string{ apiHandler = addResponseHeaders(map[string]string{
"Cache-Control": "no-store, max-age=0", "Cache-Control": "no-store, max-age=0",

50
srv/api/csrf.go Normal file
View File

@ -0,0 +1,50 @@
package api
import (
"errors"
"net/http"
)
const (
csrfTokenCookieName = "csrf_token"
csrfTokenHeaderName = "X-CSRF-Token"
)
func setCSRFMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
csrfTok, err := getCookie(r, csrfTokenCookieName, "")
if err != nil {
internalServerError(rw, r, err)
return
} else if csrfTok == "" {
http.SetCookie(rw, &http.Cookie{
Name: csrfTokenCookieName,
Value: randStr(32),
Secure: true,
})
}
h.ServeHTTP(rw, r)
})
}
func checkCSRFMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
csrfTok, err := getCookie(r, csrfTokenCookieName, "")
if err != nil {
internalServerError(rw, r, err)
return
} else if csrfTok == "" || r.Header.Get(csrfTokenHeaderName) != csrfTok {
badRequest(rw, r, errors.New("invalid CSRF token"))
return
}
h.ServeHTTP(rw, r)
})
}

View File

@ -2,7 +2,11 @@ package api
import ( import (
"context" "context"
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -66,3 +70,22 @@ func strToInt(str string, defaultVal int) (int, error) {
} }
return strconv.Atoi(str) return strconv.Atoi(str)
} }
func getCookie(r *http.Request, cookieName, defaultVal string) (string, error) {
c, err := r.Cookie(cookieName)
if errors.Is(err, http.ErrNoCookie) {
return defaultVal, nil
} else if err != nil {
return "", fmt.Errorf("reading cookie %q: %w", cookieName, err)
}
return c.Value, nil
}
func randStr(numBytesEntropy int) string {
b := make([]byte, numBytesEntropy)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}

View File

@ -1,3 +1,4 @@
import * as utils from "/assets/utils.js";
const doFetch = async (req) => { const doFetch = async (req) => {
let res, jsonRes; let res, jsonRes;
@ -48,7 +49,15 @@ const solvePow = async () => {
const call = async (method, route, opts = {}) => { const call = async (method, route, opts = {}) => {
const { body = {}, requiresPow = false } = opts; const { body = {}, requiresPow = false } = opts;
const reqOpts = { method }; if (!utils.cookies["csrf_token"])
throw "csrf_token cookie not set, can't make api call";
const reqOpts = {
method,
headers: {
"X-CSRF-Token": utils.cookies["csrf_token"],
},
};
if (requiresPow) { if (requiresPow) {
const {seed, solution} = await solvePow(); const {seed, solution} = await solvePow();
@ -57,7 +66,6 @@ const call = async (method, route, opts = {}) => {
} }
if (Object.keys(body).length > 0) { if (Object.keys(body).length > 0) {
const form = new FormData(); const form = new FormData();
for (const key in body) form.append(key, body[key]); for (const key in body) form.append(key, body[key]);

View File

@ -0,0 +1,12 @@
const cookies = {};
const cookieKVs = document.cookie
.split(';')
.map(cookie => cookie.trim().split('=', 2));
for (const i in cookieKVs) {
cookies[cookieKVs[i][0]] = cookieKVs[i][1];
}
export {
cookies,
}