Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-11-27 10:52:28
Commitb8047017f5dc43349e044cdcee1994c6e906ca47
Parentc768e5c1b66cada5b99688924036c9a4f5d1a272

Add CSRF protection middleware

Diffstat

M README.md | 4 ++--
M go.mod | 2 ++
M go.sum | 6 ++++++
M res/admin/repo_edit.html | 3 +++
M res/admin/user_create.html | 1 +
M res/admin/user_edit.html | 1 +
M res/repo/create.html | 1 +
M res/repo/edit.html | 3 +++
M res/user/edit.html | 2 ++
M res/user/login.html | 1 +
M src/admin/repos.go | 6 ++++++
M src/admin/users.go | 10 ++++++++++
M src/goit/auth.go | 6 +++++-
M src/goit/goit.go | 2 ++
M src/main.go | 8 +++++++-
M src/repo/create.go | 10 +++++++++-
M src/repo/edit.go | 6 ++++++
M src/user/edit.go | 6 ++++++
M src/user/login.go | 10 +++++++++-

19 files changed, 82 insertions, 6 deletions

diff --git a/README.md b/README.md
index 4291fd5..d205e4c 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@ A simple and lightweight Git web server.
 - Git Smart HTTP protocol (v2 only)
 - Git SSH protocol (planned)
 - Repository log, tree, refs, and commit viewers
-- File viewer with syntax highlighting (planned)
-- File raw, blame, and history views (planned)
+- File viewer with (planned) syntax highlighting
+- File log, blame (planned), and raw views
 - Public and private repositories
 - Read and write permissions for non owners (planned)
 - Repository mirroring (pull and/or push) (planned)
diff --git a/go.mod b/go.mod
index 2baa3d1..3e82a89 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/buildkite/terminal-to-html/v3 v3.9.1
 	github.com/dustin/go-humanize v1.0.1
 	github.com/go-git/go-git/v5 v5.9.0
+	github.com/gorilla/csrf v1.7.2
 	github.com/gorilla/mux v1.8.0
 	github.com/mattn/go-sqlite3 v1.14.17
 	golang.org/x/crypto v0.14.0
@@ -23,6 +24,7 @@ require (
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-git/go-billy/v5 v5.5.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/gorilla/securecookie v1.1.2 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index fcf75df..67e4c90 100644
--- a/go.sum
+++ b/go.sum
@@ -44,8 +44,14 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
+github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
diff --git a/res/admin/repo_edit.html b/res/admin/repo_edit.html
index 1b852a6..14243d0 100644
--- a/res/admin/repo_edit.html
+++ b/res/admin/repo_edit.html
@@ -5,6 +5,7 @@
 	<main>
 		<h1>{{.Title}}</h1><hr>
 		<form action="/admin/repo/edit?repo={{.Edit.Id}}" method="post">
+			{{.CsrfField}}
 			<input type="hidden" name="action" value="edit">
 			<table>
 				<tr>
@@ -48,6 +49,7 @@
 		<br><h2>Transfer Ownership</h2><hr>
 		<span>- You will lose access to this repository if it is not public.</span><br><br>
 		<form action="/admin/repo/edit?repo={{.Edit.Id}}" method="post">
+			{{.CsrfField}}
 			<input type="hidden" name="action" value="transfer">
 			<table>
 				<tr><td><label for="owner">New Owner</label></td></tr>
@@ -63,6 +65,7 @@
 		<span>- This operation <b>CANNOT</b> be undone.</span><br>
 		<span>- This operation will permanently delete the {{.Name}} repository and all associated data.</span><br><br>
 		<form action="/admin/repo/edit?repo={{.Edit.Id}}" method="post">
+			{{.CsrfField}}
 			<input type="hidden" name="action" value="delete">
 			<table>
 				<tr><td><label for="reponame">To confirm, type "{{.Name}}" in the box below</label></td></tr>
diff --git a/res/admin/user_create.html b/res/admin/user_create.html
index 7909270..0352901 100644
--- a/res/admin/user_create.html
+++ b/res/admin/user_create.html
@@ -19,6 +19,7 @@
 	<main>
 		<h1>{{.Title}}</h1><hr>
 		<form action="/admin/user/create" method="post">
+			{{.CsrfField}}
 			<table>
 				<tr><td><label for="username">Username</label></td></tr>
 				<tr><td><input type="text" name="username" value="{{.Form.Name}}" spellcheck="false"></td></tr>
diff --git a/res/admin/user_edit.html b/res/admin/user_edit.html
index 4f4f73f..2696ddb 100644
--- a/res/admin/user_edit.html
+++ b/res/admin/user_edit.html
@@ -19,6 +19,7 @@
 	<main>
 		<h1>{{.Title}}</h1><hr>
 		<form action="/admin/user/edit?user={{.Form.Id}}" method="post">
+			{{.CsrfField}}
 			<table>
 				<tr><td><label for="id">ID</label></td></tr>
 				<tr><td><span>{{.Form.Id}}</span></td></tr>
diff --git a/res/repo/create.html b/res/repo/create.html
index 753e578..20c31b1 100644
--- a/res/repo/create.html
+++ b/res/repo/create.html
@@ -12,6 +12,7 @@
 	</header>
 	<main>
 		<form action="/repo/create" method="post">
+			{{.CsrfField}}
 			<table>
 				<tr>
 					<td style="text-align: right;"><label for="reponame">Name</label></td>
diff --git a/res/repo/edit.html b/res/repo/edit.html
index 768c864..6f99739 100644
--- a/res/repo/edit.html
+++ b/res/repo/edit.html
@@ -5,6 +5,7 @@
 	<main>
 		<h1>{{.Title}}</h1><hr>
 		<form action="/{{.Name}}/edit" method="post">
+			{{.CsrfField}}
 			<input type="hidden" name="action" value="edit">
 			<table>
 				<tr>
@@ -48,6 +49,7 @@
 		<br><h2>Transfer Ownership</h2><hr>
 		<span>- You will lose access to this repository if it is not public.</span><br><br>
 		<form action="/{{.Name}}/edit" method="post">
+			{{.CsrfField}}
 			<input type="hidden" name="action" value="transfer">
 			<table>
 				<tr><td><label for="owner">New Owner</label></td></tr>
@@ -63,6 +65,7 @@
 		<span>- This operation <b>CANNOT</b> be undone.</span><br>
 		<span>- This operation will permanently delete the {{.Name}} repository and all associated data.</span><br><br>
 		<form action="/{{.Name}}/edit" method="post">
+			{{.CsrfField}}
 			<input type="hidden" name="action" value="delete">
 			<table>
 				<tr><td><label for="reponame">To confirm, type "{{.Name}}" in the box below</label></td></tr>
diff --git a/res/user/edit.html b/res/user/edit.html
index 2f8ee56..5317316 100644
--- a/res/user/edit.html
+++ b/res/user/edit.html
@@ -5,6 +5,7 @@
 	<main>
 		<h1>{{.Title}}</h1><hr>
 		<form action="/user/edit" method="post">
+			{{.CsrfField}}
 			<table>
 				<tr><td><label for="username">Username</label></td></tr>
 				<tr><td><input type="text" name="username" value="{{.Form.Name}}" spellcheck="false"></td></tr>
@@ -21,6 +22,7 @@
 			</table>
 		</form><hr>
 		<form action="/user/edit" method="post">
+			{{.CsrfField}}
 			<table>
 				<tr><td><label for="password">Current Password</label></td></tr>
 				<tr><td><input type="password" name="password"></td></tr>
diff --git a/res/user/login.html b/res/user/login.html
index 4d235cf..6bbc819 100644
--- a/res/user/login.html
+++ b/res/user/login.html
@@ -12,6 +12,7 @@
 	</header>
 	<main>
 		<form action="/user/login" method="post">
+			{{.CsrfField}}
 			<table>
 				<tr>
 					<td style="text-align: right;"><label for="username">Username</label></td>
diff --git a/src/admin/repos.go b/src/admin/repos.go
index 01d1d66..fdf7598 100644
--- a/src/admin/repos.go
+++ b/src/admin/repos.go
@@ -2,6 +2,7 @@ package admin
 
 import (
 	"fmt"
+	"html/template"
 	"log"
 	"net/http"
 	"slices"
@@ -10,6 +11,7 @@ import (
 	"github.com/Jamozed/Goit/src/goit"
 	"github.com/Jamozed/Goit/src/util"
 	"github.com/dustin/go-humanize"
+	"github.com/gorilla/csrf"
 )
 
 func HandleRepos(w http.ResponseWriter, r *http.Request) {
@@ -110,9 +112,13 @@ func HandleRepoEdit(w http.ResponseWriter, r *http.Request) {
 
 		Transfer struct{ Owner, Message string }
 		Delete   struct{ Message string }
+
+		CsrfField template.HTML
 	}{
 		Title: "Admin - Edit Repository",
 		Name:  repo.Name,
+
+		CsrfField: csrf.TemplateField(r),
 	}
 
 	data.Edit.Id = fmt.Sprint(repo.Id)
diff --git a/src/admin/users.go b/src/admin/users.go
index edf1edd..5d1a674 100644
--- a/src/admin/users.go
+++ b/src/admin/users.go
@@ -2,6 +2,7 @@ package admin
 
 import (
 	"fmt"
+	"html/template"
 	"log"
 	"net/http"
 	"slices"
@@ -10,6 +11,7 @@ import (
 
 	"github.com/Jamozed/Goit/src/goit"
 	"github.com/Jamozed/Goit/src/util"
+	"github.com/gorilla/csrf"
 )
 
 func HandleUsers(w http.ResponseWriter, r *http.Request) {
@@ -69,8 +71,12 @@ func HandleUserCreate(w http.ResponseWriter, r *http.Request) {
 			Name, FullName string
 			IsAdmin        bool
 		}
+
+		CsrfField template.HTML
 	}{
 		Title: "Admin - Create User",
+
+		CsrfField: csrf.TemplateField(r),
 	}
 
 	if r.Method == http.MethodPost {
@@ -148,8 +154,12 @@ func HandleUserEdit(w http.ResponseWriter, r *http.Request) {
 			Id, Name, FullName string
 			IsAdmin            bool
 		}
+
+		CsrfField template.HTML
 	}{
 		Title: "Admin - Edit User",
+
+		CsrfField: csrf.TemplateField(r),
 	}
 
 	data.Form.Id = fmt.Sprint(u.Id)
diff --git a/src/goit/auth.go b/src/goit/auth.go
index 108f1f2..809cf18 100644
--- a/src/goit/auth.go
+++ b/src/goit/auth.go
@@ -111,7 +111,11 @@ func CleanupSessions() {
 
 /* Set a user session cookie. */
 func SetSessionCookie(w http.ResponseWriter, uid int64, s Session) {
-	c := &http.Cookie{Name: "session", Value: fmt.Sprint(uid) + "." + s.Token, Path: "/", Expires: s.Expiry}
+	c := &http.Cookie{
+		Name: "session", Value: fmt.Sprint(uid) + "." + s.Token, Path: "/", Expires: s.Expiry,
+		Secure: util.If(Conf.UsesHttps, true, false), HttpOnly: true, SameSite: http.SameSiteLaxMode,
+	}
+
 	if err := c.Valid(); err != nil {
 		log.Println("[Cookie]", err.Error())
 	}
diff --git a/src/goit/goit.go b/src/goit/goit.go
index 25130bd..59051bf 100644
--- a/src/goit/goit.go
+++ b/src/goit/goit.go
@@ -33,6 +33,7 @@ type Config struct {
 	GitPath    string `json:"git_path"`
 	IpSessions bool   `json:"ip_sessions"`
 	UsesHttps  bool   `json:"uses_https"`
+	CsrfSecret string `json:"csrf_secret"`
 }
 
 var Conf = Config{
@@ -42,6 +43,7 @@ var Conf = Config{
 	GitPath:    "git",
 	IpSessions: true,
 	UsesHttps:  false,
+	CsrfSecret: "1234567890abcdef1234567890abcdef",
 }
 
 var db *sql.DB
diff --git a/src/main.go b/src/main.go
index d5cdf8c..26bf003 100644
--- a/src/main.go
+++ b/src/main.go
@@ -25,6 +25,7 @@ import (
 	"github.com/Jamozed/Goit/src/user"
 	"github.com/Jamozed/Goit/src/util"
 	"github.com/adrg/xdg"
+	"github.com/gorilla/csrf"
 	"github.com/gorilla/mux"
 )
 
@@ -136,8 +137,13 @@ func main() {
 	wait.Add(1)
 	go handleIpc(stop, wait, ipc)
 
+	protect := csrf.Protect(
+		[]byte(goit.Conf.CsrfSecret), csrf.FieldName("csrf.Token"), csrf.CookieName("csrf"),
+		csrf.Secure(util.If(goit.Conf.UsesHttps, true, false)),
+	)
+
 	/* Listen for HTTP on the specified port */
-	if err := http.ListenAndServe(goit.Conf.HttpAddr+":"+goit.Conf.HttpPort, logHttp(h)); err != nil {
+	if err := http.ListenAndServe(goit.Conf.HttpAddr+":"+goit.Conf.HttpPort, protect(logHttp(h))); err != nil {
 		log.Fatalln("[HTTP]", err.Error())
 	}
 }
diff --git a/src/repo/create.go b/src/repo/create.go
index af6131c..48f5a3f 100644
--- a/src/repo/create.go
+++ b/src/repo/create.go
@@ -1,11 +1,13 @@
 package repo
 
 import (
+	"html/template"
 	"log"
 	"net/http"
 	"slices"
 
 	"github.com/Jamozed/Goit/src/goit"
+	"github.com/gorilla/csrf"
 )
 
 func HandleCreate(w http.ResponseWriter, r *http.Request) {
@@ -19,7 +21,13 @@ func HandleCreate(w http.ResponseWriter, r *http.Request) {
 		Title, Message    string
 		Name, Description string
 		IsPrivate         bool
-	}{Title: "Repository - Create"}
+
+		CsrfField template.HTML
+	}{
+		Title: "Repository - Create",
+
+		CsrfField: csrf.TemplateField(r),
+	}
 
 	if r.Method == http.MethodPost {
 		data.Name = r.FormValue("reponame")
diff --git a/src/repo/edit.go b/src/repo/edit.go
index eb98841..fb24573 100644
--- a/src/repo/edit.go
+++ b/src/repo/edit.go
@@ -3,6 +3,7 @@ package repo
 import (
 	"errors"
 	"fmt"
+	"html/template"
 	"log"
 	"net/http"
 	"path/filepath"
@@ -12,6 +13,7 @@ import (
 	"github.com/Jamozed/Goit/src/util"
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/gorilla/csrf"
 	"github.com/gorilla/mux"
 )
 
@@ -62,12 +64,16 @@ func HandleEdit(w http.ResponseWriter, r *http.Request) {
 
 		Transfer struct{ Owner, Message string }
 		Delete   struct{ Message string }
+
+		CsrfField template.HTML
 	}{
 		Title:       "Repository - Edit",
 		Name:        repo.Name,
 		Description: repo.Description,
 		Url:         util.If(goit.Conf.UsesHttps, "https://", "http://") + r.Host + "/" + repo.Name,
 		Editable:    (auth && repo.OwnerId == user.Id),
+
+		CsrfField: csrf.TemplateField(r),
 	}
 
 	data.Edit.Id = fmt.Sprint(repo.Id)
diff --git a/src/user/edit.go b/src/user/edit.go
index 7ef2739..19cda31 100644
--- a/src/user/edit.go
+++ b/src/user/edit.go
@@ -3,11 +3,13 @@ package user
 import (
 	"bytes"
 	"fmt"
+	"html/template"
 	"log"
 	"net/http"
 	"slices"
 
 	"github.com/Jamozed/Goit/src/goit"
+	"github.com/gorilla/csrf"
 )
 
 func HandleEdit(w http.ResponseWriter, r *http.Request) {
@@ -32,8 +34,12 @@ func HandleEdit(w http.ResponseWriter, r *http.Request) {
 		Title, MessageA, MessageB string
 
 		Form struct{ Id, Name, FullName string }
+
+		CsrfField template.HTML
 	}{
 		Title: "User - Edit",
+
+		CsrfField: csrf.TemplateField(r),
 	}
 
 	data.Form.Id = fmt.Sprint(user.Id)
diff --git a/src/user/login.go b/src/user/login.go
index 2d031f6..a87e7fa 100644
--- a/src/user/login.go
+++ b/src/user/login.go
@@ -6,12 +6,14 @@ package user
 
 import (
 	"bytes"
+	"html/template"
 	"log"
 	"net"
 	"net/http"
 	"time"
 
 	"github.com/Jamozed/Goit/src/goit"
+	"github.com/gorilla/csrf"
 )
 
 func HandleLogin(w http.ResponseWriter, r *http.Request) {
@@ -22,7 +24,13 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) {
 	data := struct {
 		Title, Message, Name string
 		FocusPw              bool
-	}{Title: "Login"}
+
+		CsrfField template.HTML
+	}{
+		Title: "Login",
+
+		CsrfField: csrf.TemplateField(r),
+	}
 
 	if r.Method == http.MethodPost {
 		data.Name = r.FormValue("username")