diff --git a/pkg/webui/accounts.go b/pkg/webui/accounts.go index dfc4439..ad0dbca 100644 --- a/pkg/webui/accounts.go +++ b/pkg/webui/accounts.go @@ -1,6 +1,7 @@ package webui import ( + "fmt" "html/template" "net/http" "path" @@ -36,6 +37,14 @@ func handleAccountsGET(db *database.DB) http.Handler { func handleAccountsPOST(db *database.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + errorResponse(w, r, http.StatusBadRequest, + fmt.Errorf("failed to parse form: %w", err)) + return + } + if !verifyCSRFToken(w, r) { + return + } accounts, err := db.LoadAccounts() if err != nil { errorResponse(w, r, http.StatusInternalServerError, err) diff --git a/pkg/webui/accountsIdResetPassword.go b/pkg/webui/accountsIdResetPassword.go index a39d287..9c5b6f5 100644 --- a/pkg/webui/accountsIdResetPassword.go +++ b/pkg/webui/accountsIdResetPassword.go @@ -1,6 +1,7 @@ package webui import ( + "fmt" "html/template" "net/http" @@ -66,6 +67,14 @@ func handleAccountsIdResetPasswordPOST(db *database.DB) http.Handler { if account == nil { return } + if err := r.ParseForm(); err != nil { + errorResponse(w, r, http.StatusBadRequest, + fmt.Errorf("failed to parse form: %w", err)) + return + } + if !verifyCSRFToken(w, r) { + return + } password := r.FormValue("password") if len(password) < 8 { errorResponse(w, r, http.StatusBadRequest, nil) diff --git a/pkg/webui/csrf.go b/pkg/webui/csrf.go new file mode 100644 index 0000000..7261f84 --- /dev/null +++ b/pkg/webui/csrf.go @@ -0,0 +1,30 @@ +package webui + +import ( + "fmt" + "net/http" + + "git.adyxax.org/adyxax/tfstated/pkg/model" + "go.n16f.net/uuid" +) + +func verifyCSRFToken(w http.ResponseWriter, r *http.Request) bool { + session := r.Context().Value(model.SessionContextKey{}).(*model.Session) + tokenStr := r.FormValue("csrf_token") + if tokenStr == "" { + tokenStr = r.Header.Get("X-XSRF-Token") + } + var token uuid.UUID + if err := token.Parse(tokenStr); err != nil { + errorResponse(w, r, http.StatusBadRequest, + fmt.Errorf("failed to parse csrf token: %w", err)) + return false + } + + if !token.Equal(session.Data.CsrfToken) { + errorResponse(w, r, http.StatusForbidden, + fmt.Errorf("invalid csrf token")) + return false + } + return true +} diff --git a/pkg/webui/html/accounts.html b/pkg/webui/html/accounts.html index ff3670a..227585e 100644 --- a/pkg/webui/html/accounts.html +++ b/pkg/webui/html/accounts.html @@ -9,6 +9,7 @@ {{ if .Page.Session.Data.Account.IsAdmin }}
+
New User Account
@@ -40,6 +41,7 @@ {{ end }}
+
{{ end }} diff --git a/pkg/webui/html/accountsId.html b/pkg/webui/html/accountsId.html index 1ae7902..fe05cbf 100644 --- a/pkg/webui/html/accountsId.html +++ b/pkg/webui/html/accountsId.html @@ -27,6 +27,7 @@ {{ if .Page.Session.Data.Account.IsAdmin }}

Operations

+
Edit User Account diff --git a/pkg/webui/html/accountsIdResetPassword.html b/pkg/webui/html/accountsIdResetPassword.html index c1f348f..87ced7a 100644 --- a/pkg/webui/html/accountsIdResetPassword.html +++ b/pkg/webui/html/accountsIdResetPassword.html @@ -8,6 +8,7 @@

User Account

Password Reset

+
Set Password

diff --git a/pkg/webui/html/login.html b/pkg/webui/html/login.html index c526cfe..1fc6335 100644 --- a/pkg/webui/html/login.html +++ b/pkg/webui/html/login.html @@ -1,6 +1,7 @@ {{ define "main" }}

+
Login
diff --git a/pkg/webui/html/settings.html b/pkg/webui/html/settings.html index bba3bae..8c6ea17 100644 --- a/pkg/webui/html/settings.html +++ b/pkg/webui/html/settings.html @@ -1,6 +1,7 @@ {{ define "main" }}

Settings

+
Account Settings
diff --git a/pkg/webui/html/states.html b/pkg/webui/html/states.html index f55baa0..6deeb61 100644 --- a/pkg/webui/html/states.html +++ b/pkg/webui/html/states.html @@ -7,6 +7,7 @@

You also have the option to upload a JSON state file in order to create a new state in TfStated. This is equivalent to using the state push command of OpenTofu/Terraform on a brand new state.

+
New State
diff --git a/pkg/webui/html/statesId.html b/pkg/webui/html/statesId.html index 46610a0..d8bb9ea 100644 --- a/pkg/webui/html/statesId.html +++ b/pkg/webui/html/statesId.html @@ -24,6 +24,7 @@

Operations

+
Edit State
@@ -60,6 +61,7 @@
+
Danger Zone diff --git a/pkg/webui/login.go b/pkg/webui/login.go index 42b403e..3df3000 100644 --- a/pkg/webui/login.go +++ b/pkg/webui/login.go @@ -52,6 +52,9 @@ func handleLoginPOST(db *database.DB) http.Handler { fmt.Errorf("failed to parse form: %w", err)) return } + if !verifyCSRFToken(w, r) { + return + } username := r.FormValue("username") password := r.FormValue("password") diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go index 6648a1b..f21fd83 100644 --- a/pkg/webui/routes.go +++ b/pkg/webui/routes.go @@ -22,7 +22,7 @@ func addRoutes( mux.Handle("GET /login", requireSession(handleLoginGET())) mux.Handle("POST /login", requireSession(handleLoginPOST(db))) mux.Handle("GET /logout", requireLogin(handleLogoutGET(db))) - mux.Handle("GET /settings", requireLogin(handleSettingsGET(db))) + mux.Handle("GET /settings", requireLogin(handleSettingsGET())) mux.Handle("POST /settings", requireLogin(handleSettingsPOST(db))) mux.Handle("GET /states", requireLogin(handleStatesGET(db))) mux.Handle("POST /states", requireLogin(handleStatesPOST(db))) diff --git a/pkg/webui/settings.go b/pkg/webui/settings.go index d2ff999..cb65068 100644 --- a/pkg/webui/settings.go +++ b/pkg/webui/settings.go @@ -16,7 +16,7 @@ type SettingsPage struct { var settingsTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/settings.html")) -func handleSettingsGET(db *database.DB) http.Handler { +func handleSettingsGET() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { render(w, settingsTemplates, http.StatusOK, SettingsPage{ Page: makePage(r, &Page{Title: "Settings", Section: "settings"}), @@ -30,6 +30,9 @@ func handleSettingsPOST(db *database.DB) http.Handler { errorResponse(w, r, http.StatusBadRequest, err) return } + if !verifyCSRFToken(w, r) { + return + } darkMode := r.FormValue("dark-mode") settings := model.Settings{ LightMode: darkMode != "1", diff --git a/pkg/webui/states.go b/pkg/webui/states.go index 2f872cb..c5ffbc8 100644 --- a/pkg/webui/states.go +++ b/pkg/webui/states.go @@ -44,6 +44,9 @@ func handleStatesPOST(db *database.DB) http.Handler { errorResponse(w, r, http.StatusBadRequest, err) return } + if !verifyCSRFToken(w, r) { + return + } file, _, err := r.FormFile("file") if err != nil { errorResponse(w, r, http.StatusBadRequest, err) diff --git a/pkg/webui/statesId.go b/pkg/webui/statesId.go index 8127db5..38a5cee 100644 --- a/pkg/webui/statesId.go +++ b/pkg/webui/statesId.go @@ -63,6 +63,9 @@ func handleStatesIdPOST(db *database.DB) http.Handler { errorResponse(w, r, http.StatusBadRequest, err) return } + if !verifyCSRFToken(w, r) { + return + } action := r.FormValue("action") var stateId uuid.UUID if err := stateId.Parse(r.PathValue("id")); err != nil {