diff --git a/Gopkg.lock b/Gopkg.lock index d28aa84..41e1cfc 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,12 @@ revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" version = "v1.1" +[[projects]] + name = "github.com/gorilla/csrf" + packages = ["."] + revision = "69581736821c33d85bbf378f42f6ad864dbd85de" + version = "v1.5" + [[projects]] name = "github.com/gorilla/securecookie" packages = ["."] @@ -70,6 +76,12 @@ revision = "b2cb9fa56473e98db8caba80237377e83fe44db5" version = "v1" +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] branch = "master" name = "github.com/rubenv/sql-migrate" @@ -121,6 +133,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c6097211865a7f2c761915a6166499eccdb244c65adf046866ac3baed8f6a838" + inputs-digest = "c886392b015af75b1a55b8f7ed686da847be1a2d340aa14c4183e6d273f94c65" solver-name = "gps-cdcl" solver-version = 1 diff --git a/boardvoting.go b/boardvoting.go index a0d6359..e8a83e4 100644 --- a/boardvoting.go +++ b/boardvoting.go @@ -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", "
", -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) diff --git a/boardvoting/templates/create_motion_form.html b/boardvoting/templates/create_motion_form.html index 5958aa7..ef72ec6 100644 --- a/boardvoting/templates/create_motion_form.html +++ b/boardvoting/templates/create_motion_form.html @@ -9,6 +9,7 @@
+ {{ csrfField }}
diff --git a/boardvoting/templates/direct_vote_form.html b/boardvoting/templates/direct_vote_form.html index 649c059..66e21a9 100644 --- a/boardvoting/templates/direct_vote_form.html +++ b/boardvoting/templates/direct_vote_form.html @@ -9,19 +9,23 @@ {{ with .Decision }}
- {{ template "motion_fragment" . }} + {{ template "motion_fragment" . }}
{{ end }} +{{ csrfField }}
- {{ if eq 1 .VoteChoice }} - - {{ else if eq -1 .VoteChoice }} - - {{ else }} - - {{ end }} + {{ if eq 1 .VoteChoice }} + + {{ else if eq -1 .VoteChoice }} + + {{ else }} + + {{ end }}
{{ template "footer.html" . }} \ No newline at end of file diff --git a/boardvoting/templates/proxy_vote_form.html b/boardvoting/templates/proxy_vote_form.html index 3a344c3..97ae86b 100644 --- a/boardvoting/templates/proxy_vote_form.html +++ b/boardvoting/templates/proxy_vote_form.html @@ -10,20 +10,21 @@
- {{ with .Decision }} + {{ with .Decision }} {{ template "motion_fragment" . }} {{ end }}
+ {{ csrfField }}
@@ -39,14 +40,15 @@
- {{ with .Form.Errors }} + {{ with .Form.Errors }}
- {{ with .Voter }}

{{ . }}

{{ end }} - {{ with .Vote }}

{{ . }}

{{ end }} - {{ with .Justification }}

{{ . }}

{{ end }} + {{ with .Voter }}

{{ . }}

{{ end }} + {{ with .Vote }}

{{ . }}

{{ end }} + {{ with .Justification }}

{{ . }}

{{ end }}
- {{ end }} - + {{ end }} +
diff --git a/boardvoting/templates/withdraw_motion_form.html b/boardvoting/templates/withdraw_motion_form.html index 87ebb57..7b444e7 100644 --- a/boardvoting/templates/withdraw_motion_form.html +++ b/boardvoting/templates/withdraw_motion_form.html @@ -9,11 +9,12 @@ {{ with .Decision }}
- {{ template "motion_fragment" . }} + {{ template "motion_fragment" . }}
{{ end }}
+{{ csrfField }}
diff --git a/config.yaml.example b/config.yaml.example index 19e96ea..7d10163 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -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: