Implement CSRF protection
authorJan Dittberner <jan@dittberner.info>
Sat, 31 Mar 2018 08:50:06 +0000 (10:50 +0200)
committerJan Dittberner <jan@dittberner.info>
Sat, 31 Mar 2018 08:50:06 +0000 (10:50 +0200)
Gopkg.lock
boardvoting.go
boardvoting/templates/create_motion_form.html
boardvoting/templates/direct_vote_form.html
boardvoting/templates/proxy_vote_form.html
boardvoting/templates/withdraw_motion_form.html
config.yaml.example

index d28aa84..41e1cfc 100644 (file)
   version = "v1.1"
 
 [[projects]]
+  name = "github.com/gorilla/csrf"
+  packages = ["."]
+  revision = "69581736821c33d85bbf378f42f6ad864dbd85de"
+  version = "v1.5"
+
+[[projects]]
   name = "github.com/gorilla/securecookie"
   packages = ["."]
   revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983"
   version = "v1"
 
 [[projects]]
+  name = "github.com/pkg/errors"
+  packages = ["."]
+  revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
+  version = "v0.8.0"
+
+[[projects]]
   branch = "master"
   name = "github.com/rubenv/sql-migrate"
   packages = [
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "c6097211865a7f2c761915a6166499eccdb244c65adf046866ac3baed8f6a838"
+  inputs-digest = "c886392b015af75b1a55b8f7ed686da847be1a2d340aa14c4183e6d273f94c65"
   solver-name = "gps-cdcl"
   solver-version = 1
index a0d6359..e8a83e4 100644 (file)
@@ -10,12 +10,6 @@ import (
        "encoding/pem"
        "flag"
        "fmt"
-       "git.cacert.org/cacert-boardvoting/boardvoting"
-       "github.com/Masterminds/sprig"
-       "github.com/gorilla/sessions"
-       _ "github.com/mattn/go-sqlite3"
-       "github.com/op/go-logging"
-       "gopkg.in/yaml.v2"
        "html/template"
        "io/ioutil"
        "net/http"
@@ -24,22 +18,35 @@ import (
        "strconv"
        "strings"
        "time"
+
+       "github.com/Masterminds/sprig"
+       "github.com/gorilla/csrf"
+       "github.com/gorilla/sessions"
+       _ "github.com/mattn/go-sqlite3"
+       "github.com/op/go-logging"
+       "gopkg.in/yaml.v2"
+
+       "git.cacert.org/cacert-boardvoting/boardvoting"
 )
 
 var configFile string
 var config *Config
 var store *sessions.CookieStore
+var csrfKey []byte
 var version = "undefined"
 var build = "undefined"
 var log *logging.Logger
 
 const sessionCookieName = "votesession"
 
-func renderTemplate(w http.ResponseWriter, templates []string, context interface{}) {
+func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
        funcMaps := sprig.FuncMap()
        funcMaps["nl2br"] = func(text string) template.HTML {
                return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
        }
+       funcMaps[csrf.TemplateTag] = func() template.HTML {
+               return csrf.TemplateField(r)
+       }
 
        var baseTemplate *template.Template
 
@@ -67,7 +74,7 @@ func renderTemplate(w http.ResponseWriter, templates []string, context interface
 type contextKey int
 
 const (
-       ctxNeedsAuth contextKey = iota
+       ctxNeedsAuth         contextKey = iota
        ctxVoter
        ctxDecision
        ctxVote
@@ -110,7 +117,7 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht
                }
                sort.Strings(templateContext.Emails)
                w.WriteHeader(http.StatusForbidden)
-               renderTemplate(w, []string{"denied.html", "header.html", "footer.html"}, templateContext)
+               renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
                return
        }
        handler(w, r)
@@ -121,7 +128,7 @@ type motionParameters struct {
 }
 
 type motionListParameters struct {
-       Page  int64
+       Page int64
        Flags struct {
                Confirmed, Withdraw, Unvoted bool
        }
@@ -194,7 +201,7 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
                templateContext.PrevPage = params.Page - 1
        }
 
-       renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
+       renderTemplate(w, r, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
 }
 
 func motionHandler(w http.ResponseWriter, r *http.Request) {
@@ -227,7 +234,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
        }
        templateContext.Decision = decision
        templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
-       renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
+       renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext)
 }
 
 func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
@@ -307,6 +314,7 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                PageTitle string
                Decision  *DecisionForDisplay
                Flashes   interface{}
+               Voter     *Voter
        }
 
        switch r.Method {
@@ -326,7 +334,8 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
        default:
                templateContext.Decision = decision
-               renderTemplate(w, templates, templateContext)
+               templateContext.Voter = voter
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
@@ -359,7 +368,7 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
                if valid, data := form.Validate(); !valid {
                        templateContext.Voter = voter
                        templateContext.Form = form
-                       renderTemplate(w, templates, templateContext)
+                       renderTemplate(w, r, templates, templateContext)
                } else {
                        data.Proposed = time.Now().UTC()
                        data.ProponentId = voter.Id
@@ -382,7 +391,7 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
                templateContext.Form = NewDecisionForm{
                        VoteType: strconv.FormatInt(voteTypeMotion, 10),
                }
-               renderTemplate(w, templates, templateContext)
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
@@ -422,7 +431,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                if valid, data := form.Validate(); !valid {
                        templateContext.Voter = voter
                        templateContext.Form = form
-                       renderTemplate(w, templates, templateContext)
+                       renderTemplate(w, r, templates, templateContext)
                } else {
                        data.Modified = time.Now().UTC()
                        if err := data.Update(); err != nil {
@@ -446,7 +455,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                        VoteType: fmt.Sprintf("%d", decision.VoteType),
                        Decision: &decision.Decision,
                }
-               renderTemplate(w, templates, templateContext)
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
@@ -521,7 +530,7 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
        case http.MethodPost:
                voteResult := &Vote{
                        VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
-                       Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
+                       Notes:   fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
                if err := voteResult.Save(); err != nil {
                        log.Errorf("Problem saving vote: %v", err)
                        http.Error(w, "Problem saving vote", http.StatusInternalServerError)
@@ -540,10 +549,12 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
                        VoteChoice VoteChoice
                        PageTitle  string
                        Flashes    interface{}
+                       Voter      *Voter
                }
                templateContext.Decision = decision
                templateContext.VoteChoice = vote
-               renderTemplate(w, templates, templateContext)
+               templateContext.Voter = voter
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
@@ -577,7 +588,9 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
                Voters    *[]Voter
                PageTitle string
                Flashes   interface{}
+               Voter     *Voter
        }
+       templateContext.Voter = proxy
        switch r.Method {
        case http.MethodPost:
                form := ProxyVoteForm{
@@ -595,7 +608,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
                        } else {
                                templateContext.Voters = voters
                        }
-                       renderTemplate(w, templates, templateContext)
+                       renderTemplate(w, r, templates, templateContext)
                } else {
                        data.DecisionId = decision.Id
                        data.Voted = time.Now().UTC()
@@ -625,7 +638,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
                } else {
                        templateContext.Voters = voters
                }
-               renderTemplate(w, templates, templateContext)
+               renderTemplate(w, r, templates, templateContext)
        }
 }
 
@@ -674,11 +687,12 @@ type Config struct {
        ServerCert                string `yaml:"server_certificate"`
        ServerKey                 string `yaml:"server_key"`
        CookieSecret              string `yaml:"cookie_secret"`
+       CsrfKey                   string `yaml:"csrf_key"`
        BaseURL                   string `yaml:"base_url"`
        MigrationsPath            string `yaml:"migrations_path"`
        HttpAddress               string `yaml:"http_address"`
        HttpsAddress              string `yaml:"https_address"`
-       MailServer                struct {
+       MailServer struct {
                Host string `yaml:"host"`
                Port int    `yaml:"port"`
        } `yaml:"mail_server"`
@@ -724,10 +738,10 @@ func readConfig() {
        }
 
        if config.HttpsAddress == "" {
-               config.HttpsAddress = ":8443"
+               config.HttpsAddress = "127.0.0.1:8443"
        }
        if config.HttpAddress == "" {
-               config.HttpAddress = ":8080"
+               config.HttpAddress = "127.0.0.1:8080"
        }
 
        cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
@@ -738,6 +752,13 @@ func readConfig() {
        if len(cookieSecret) < 32 {
                log.Panic("Cookie secret is less than 32 bytes long")
        }
+       csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
+       if err != nil {
+               log.Panicf("Decoding csrf key failed: %v", err)
+       }
+       if len(csrfKey) != 32 {
+               log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
+       }
        store = sessions.NewCookieStore(cookieSecret)
        log.Info("Read configuration")
 }
@@ -838,6 +859,8 @@ func main() {
                TLSConfig: tlsConfig,
        }
 
+       server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
+
        log.Infof("Launching application on https://%s/", server.Addr)
 
        errs := make(chan error, 1)
index 5958aa7..ef72ec6 100644 (file)
@@ -9,6 +9,7 @@
 <div class="column">
     <div class="ui raised segment">
         <form action="/newmotion/" method="post">
+        {{ csrfField }}
             <div class="ui form{{ if .Form.Errors }} error{{ end }}">
                 <div class="three fields">
                     <div class="field">
index 649c059..66e21a9 100644 (file)
@@ -9,19 +9,23 @@
 {{ with .Decision }}
 <div class="column">
     <div class="ui raised segment">
-        {{ template "motion_fragment" . }}
+    {{ template "motion_fragment" . }}
     </div>
 </div>
 {{ end }}
 <form action="/vote/{{ .Decision.Tag }}/{{ .VoteChoice }}" method="post">
+{{ csrfField }}
     <div class="ui form">
-        {{ if eq 1 .VoteChoice }}
-        <button class="ui right labeled green icon button" type="submit"><i class="check circle icon"></i> Vote {{ .VoteChoice }}</button>
-        {{ else if eq -1 .VoteChoice }}
-        <button class="ui right labeled red icon button" type="submit"><i class="minus circle icon"></i> Vote {{ .VoteChoice }}</button>
-        {{ else }}
-        <button class="ui right labeled grey icon button" type="submit"><i class="circle icon"></i> Vote {{ .VoteChoice }}</button>
-        {{ end }}
+    {{ if eq 1 .VoteChoice }}
+        <button class="ui right labeled green icon button" type="submit"><i class="check circle icon"></i>
+            Vote {{ .VoteChoice }}</button>
+    {{ else if eq -1 .VoteChoice }}
+        <button class="ui right labeled red icon button" type="submit"><i class="minus circle icon"></i>
+            Vote {{ .VoteChoice }}</button>
+    {{ else }}
+        <button class="ui right labeled grey icon button" type="submit"><i class="circle icon"></i>
+            Vote {{ .VoteChoice }}</button>
+    {{ end }}
     </div>
 </form>
 {{ template "footer.html" . }}
\ No newline at end of file
index 3a344c3..97ae86b 100644 (file)
 </div>
 <div class="column">
     <div class="ui raised segment">
-        {{ with .Decision }}
+    {{ with .Decision }}
         {{ template "motion_fragment" . }}
         {{ end }}
         <form action="/proxy/{{ .Decision.Tag }}" method="post">
+        {{ csrfField }}
             <div class="ui form{{ if .Form.Errors }} error{{ end }}">
                 <div class="two fields">
                     <div class="required field{{ if .Form.Errors.Voter }} error{{ end }}">
                         <label for="Voter">Voter</label>
                         <select name="Voter">
-                            {{ range .Voters }}
+                        {{ range .Voters }}
                             <option value="{{ .Id }}"
-                                    {{ if eq (.Id | print) $form.Voter }}
+                            {{ if eq (.Id | print) $form.Voter }}
                                     selected{{ end }}>{{ .Name }}</option>
-                            {{ end }}
+                        {{ end }}
                         </select>
                     </div>
                     <div class="required field{{ if .Form.Errors.Vote }} error{{ end }}">
                     <label for="Justification">Justification</label>
                     <textarea name="Justification" rows="2">{{ .Form.Justification }}</textarea>
                 </div>
-                {{ with .Form.Errors }}
+            {{ with .Form.Errors }}
                 <div class="ui error message">
-                    {{ with .Voter }}<p>{{ . }}</p>{{ end }}
-                    {{ with .Vote }}<p>{{ . }}</p>{{ end }}
-                    {{ with .Justification }}<p>{{ . }}</p>{{ end }}
+                {{ with .Voter }}<p>{{ . }}</p>{{ end }}
+                {{ with .Vote }}<p>{{ . }}</p>{{ end }}
+                {{ with .Justification }}<p>{{ . }}</p>{{ end }}
                 </div>
-                {{ end }}
-                <button class="ui primary left labeled icon button" type="submit"><i class="users icon"></i> Proxy Vote</button>
+            {{ end }}
+                <button class="ui primary left labeled icon button" type="submit"><i class="users icon"></i> Proxy Vote
+                </button>
             </div>
         </form>
     </div>
index 87ebb57..7b444e7 100644 (file)
@@ -9,11 +9,12 @@
 {{ with .Decision }}
 <div class="column">
     <div class="ui raised segment">
-        {{ template "motion_fragment" . }}
+    {{ template "motion_fragment" . }}
     </div>
 </div>
 {{ end }}
 <form action="/motions/{{ .Decision.Tag }}/withdraw" method="post">
+{{ csrfField }}
     <div class="ui form">
         <button class="ui primary left labeled icon button" type="submit"><i class="trash icon"></i> Withdraw</button>
     </div>
index 19e96ea..7d10163 100644 (file)
@@ -7,6 +7,7 @@ client_ca_certificates: cacert_class3.pem
 server_certificate: server.crt
 server_key: server.key
 cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
+csrf_key: base64encoded_random_byte_value_of_at_least_32_bytes
 base_url: https://motions.cacert.org
 migrations_path: db
 mail_server: