Implement CSRF protection

debian
Jan Dittberner 7 years ago
parent 33f75bdf1d
commit 5977eb5a7a

14
Gopkg.lock generated

@ -25,6 +25,12 @@
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1" version = "v1.1"
[[projects]]
name = "github.com/gorilla/csrf"
packages = ["."]
revision = "69581736821c33d85bbf378f42f6ad864dbd85de"
version = "v1.5"
[[projects]] [[projects]]
name = "github.com/gorilla/securecookie" name = "github.com/gorilla/securecookie"
packages = ["."] packages = ["."]
@ -70,6 +76,12 @@
revision = "b2cb9fa56473e98db8caba80237377e83fe44db5" revision = "b2cb9fa56473e98db8caba80237377e83fe44db5"
version = "v1" version = "v1"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/rubenv/sql-migrate" name = "github.com/rubenv/sql-migrate"
@ -121,6 +133,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "c6097211865a7f2c761915a6166499eccdb244c65adf046866ac3baed8f6a838" inputs-digest = "c886392b015af75b1a55b8f7ed686da847be1a2d340aa14c4183e6d273f94c65"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

@ -10,12 +10,6 @@ import (
"encoding/pem" "encoding/pem"
"flag" "flag"
"fmt" "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" "html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -24,22 +18,35 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "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 configFile string
var config *Config var config *Config
var store *sessions.CookieStore var store *sessions.CookieStore
var csrfKey []byte
var version = "undefined" var version = "undefined"
var build = "undefined" var build = "undefined"
var log *logging.Logger var log *logging.Logger
const sessionCookieName = "votesession" 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 := sprig.FuncMap()
funcMaps["nl2br"] = func(text string) template.HTML { funcMaps["nl2br"] = func(text string) template.HTML {
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1)) 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 var baseTemplate *template.Template
@ -67,7 +74,7 @@ func renderTemplate(w http.ResponseWriter, templates []string, context interface
type contextKey int type contextKey int
const ( const (
ctxNeedsAuth contextKey = iota ctxNeedsAuth contextKey = iota
ctxVoter ctxVoter
ctxDecision ctxDecision
ctxVote ctxVote
@ -110,7 +117,7 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht
} }
sort.Strings(templateContext.Emails) sort.Strings(templateContext.Emails)
w.WriteHeader(http.StatusForbidden) 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 return
} }
handler(w, r) handler(w, r)
@ -121,7 +128,7 @@ type motionParameters struct {
} }
type motionListParameters struct { type motionListParameters struct {
Page int64 Page int64
Flags struct { Flags struct {
Confirmed, Withdraw, Unvoted bool Confirmed, Withdraw, Unvoted bool
} }
@ -194,7 +201,7 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
templateContext.PrevPage = params.Page - 1 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) { func motionHandler(w http.ResponseWriter, r *http.Request) {
@ -227,7 +234,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
} }
templateContext.Decision = decision templateContext.Decision = decision
templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) 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)) { 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 PageTitle string
Decision *DecisionForDisplay Decision *DecisionForDisplay
Flashes interface{} Flashes interface{}
Voter *Voter
} }
switch r.Method { switch r.Method {
@ -326,7 +334,8 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
default: default:
templateContext.Decision = decision 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 { if valid, data := form.Validate(); !valid {
templateContext.Voter = voter templateContext.Voter = voter
templateContext.Form = form templateContext.Form = form
renderTemplate(w, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} else { } else {
data.Proposed = time.Now().UTC() data.Proposed = time.Now().UTC()
data.ProponentId = voter.Id data.ProponentId = voter.Id
@ -382,7 +391,7 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
templateContext.Form = NewDecisionForm{ templateContext.Form = NewDecisionForm{
VoteType: strconv.FormatInt(voteTypeMotion, 10), 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 { if valid, data := form.Validate(); !valid {
templateContext.Voter = voter templateContext.Voter = voter
templateContext.Form = form templateContext.Form = form
renderTemplate(w, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} else { } else {
data.Modified = time.Now().UTC() data.Modified = time.Now().UTC()
if err := data.Update(); err != nil { 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), VoteType: fmt.Sprintf("%d", decision.VoteType),
Decision: &decision.Decision, 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: case http.MethodPost:
voteResult := &Vote{ voteResult := &Vote{
VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(), 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 { if err := voteResult.Save(); err != nil {
log.Errorf("Problem saving vote: %v", err) log.Errorf("Problem saving vote: %v", err)
http.Error(w, "Problem saving vote", http.StatusInternalServerError) http.Error(w, "Problem saving vote", http.StatusInternalServerError)
@ -540,10 +549,12 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
VoteChoice VoteChoice VoteChoice VoteChoice
PageTitle string PageTitle string
Flashes interface{} Flashes interface{}
Voter *Voter
} }
templateContext.Decision = decision templateContext.Decision = decision
templateContext.VoteChoice = vote 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 Voters *[]Voter
PageTitle string PageTitle string
Flashes interface{} Flashes interface{}
Voter *Voter
} }
templateContext.Voter = proxy
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
form := ProxyVoteForm{ form := ProxyVoteForm{
@ -595,7 +608,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
} else { } else {
templateContext.Voters = voters templateContext.Voters = voters
} }
renderTemplate(w, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} else { } else {
data.DecisionId = decision.Id data.DecisionId = decision.Id
data.Voted = time.Now().UTC() data.Voted = time.Now().UTC()
@ -625,7 +638,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
} else { } else {
templateContext.Voters = voters templateContext.Voters = voters
} }
renderTemplate(w, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} }
} }
@ -674,11 +687,12 @@ type Config struct {
ServerCert string `yaml:"server_certificate"` ServerCert string `yaml:"server_certificate"`
ServerKey string `yaml:"server_key"` ServerKey string `yaml:"server_key"`
CookieSecret string `yaml:"cookie_secret"` CookieSecret string `yaml:"cookie_secret"`
CsrfKey string `yaml:"csrf_key"`
BaseURL string `yaml:"base_url"` BaseURL string `yaml:"base_url"`
MigrationsPath string `yaml:"migrations_path"` MigrationsPath string `yaml:"migrations_path"`
HttpAddress string `yaml:"http_address"` HttpAddress string `yaml:"http_address"`
HttpsAddress string `yaml:"https_address"` HttpsAddress string `yaml:"https_address"`
MailServer struct { MailServer struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
} `yaml:"mail_server"` } `yaml:"mail_server"`
@ -724,10 +738,10 @@ func readConfig() {
} }
if config.HttpsAddress == "" { if config.HttpsAddress == "" {
config.HttpsAddress = ":8443" config.HttpsAddress = "127.0.0.1:8443"
} }
if config.HttpAddress == "" { if config.HttpAddress == "" {
config.HttpAddress = ":8080" config.HttpAddress = "127.0.0.1:8080"
} }
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret) cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
@ -738,6 +752,13 @@ func readConfig() {
if len(cookieSecret) < 32 { if len(cookieSecret) < 32 {
log.Panic("Cookie secret is less than 32 bytes long") 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) store = sessions.NewCookieStore(cookieSecret)
log.Info("Read configuration") log.Info("Read configuration")
} }
@ -838,6 +859,8 @@ func main() {
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
log.Infof("Launching application on https://%s/", server.Addr) log.Infof("Launching application on https://%s/", server.Addr)
errs := make(chan error, 1) errs := make(chan error, 1)

@ -9,6 +9,7 @@
<div class="column"> <div class="column">
<div class="ui raised segment"> <div class="ui raised segment">
<form action="/newmotion/" method="post"> <form action="/newmotion/" method="post">
{{ csrfField }}
<div class="ui form{{ if .Form.Errors }} error{{ end }}"> <div class="ui form{{ if .Form.Errors }} error{{ end }}">
<div class="three fields"> <div class="three fields">
<div class="field"> <div class="field">

@ -9,19 +9,23 @@
{{ with .Decision }} {{ with .Decision }}
<div class="column"> <div class="column">
<div class="ui raised segment"> <div class="ui raised segment">
{{ template "motion_fragment" . }} {{ template "motion_fragment" . }}
</div> </div>
</div> </div>
{{ end }} {{ end }}
<form action="/vote/{{ .Decision.Tag }}/{{ .VoteChoice }}" method="post"> <form action="/vote/{{ .Decision.Tag }}/{{ .VoteChoice }}" method="post">
{{ csrfField }}
<div class="ui form"> <div class="ui form">
{{ if eq 1 .VoteChoice }} {{ if eq 1 .VoteChoice }}
<button class="ui right labeled green icon button" type="submit"><i class="check circle icon"></i> Vote {{ .VoteChoice }}</button> <button class="ui right labeled green icon button" type="submit"><i class="check circle icon"></i>
{{ else if eq -1 .VoteChoice }} Vote {{ .VoteChoice }}</button>
<button class="ui right labeled red icon button" type="submit"><i class="minus circle icon"></i> Vote {{ .VoteChoice }}</button> {{ else if eq -1 .VoteChoice }}
{{ else }} <button class="ui right labeled red icon button" type="submit"><i class="minus circle icon"></i>
<button class="ui right labeled grey icon button" type="submit"><i class="circle icon"></i> Vote {{ .VoteChoice }}</button> Vote {{ .VoteChoice }}</button>
{{ end }} {{ else }}
<button class="ui right labeled grey icon button" type="submit"><i class="circle icon"></i>
Vote {{ .VoteChoice }}</button>
{{ end }}
</div> </div>
</form> </form>
{{ template "footer.html" . }} {{ template "footer.html" . }}

@ -10,20 +10,21 @@
</div> </div>
<div class="column"> <div class="column">
<div class="ui raised segment"> <div class="ui raised segment">
{{ with .Decision }} {{ with .Decision }}
{{ template "motion_fragment" . }} {{ template "motion_fragment" . }}
{{ end }} {{ end }}
<form action="/proxy/{{ .Decision.Tag }}" method="post"> <form action="/proxy/{{ .Decision.Tag }}" method="post">
{{ csrfField }}
<div class="ui form{{ if .Form.Errors }} error{{ end }}"> <div class="ui form{{ if .Form.Errors }} error{{ end }}">
<div class="two fields"> <div class="two fields">
<div class="required field{{ if .Form.Errors.Voter }} error{{ end }}"> <div class="required field{{ if .Form.Errors.Voter }} error{{ end }}">
<label for="Voter">Voter</label> <label for="Voter">Voter</label>
<select name="Voter"> <select name="Voter">
{{ range .Voters }} {{ range .Voters }}
<option value="{{ .Id }}" <option value="{{ .Id }}"
{{ if eq (.Id | print) $form.Voter }} {{ if eq (.Id | print) $form.Voter }}
selected{{ end }}>{{ .Name }}</option> selected{{ end }}>{{ .Name }}</option>
{{ end }} {{ end }}
</select> </select>
</div> </div>
<div class="required field{{ if .Form.Errors.Vote }} error{{ end }}"> <div class="required field{{ if .Form.Errors.Vote }} error{{ end }}">
@ -39,14 +40,15 @@
<label for="Justification">Justification</label> <label for="Justification">Justification</label>
<textarea name="Justification" rows="2">{{ .Form.Justification }}</textarea> <textarea name="Justification" rows="2">{{ .Form.Justification }}</textarea>
</div> </div>
{{ with .Form.Errors }} {{ with .Form.Errors }}
<div class="ui error message"> <div class="ui error message">
{{ with .Voter }}<p>{{ . }}</p>{{ end }} {{ with .Voter }}<p>{{ . }}</p>{{ end }}
{{ with .Vote }}<p>{{ . }}</p>{{ end }} {{ with .Vote }}<p>{{ . }}</p>{{ end }}
{{ with .Justification }}<p>{{ . }}</p>{{ end }} {{ with .Justification }}<p>{{ . }}</p>{{ end }}
</div> </div>
{{ end }} {{ end }}
<button class="ui primary left labeled icon button" type="submit"><i class="users icon"></i> Proxy Vote</button> <button class="ui primary left labeled icon button" type="submit"><i class="users icon"></i> Proxy Vote
</button>
</div> </div>
</form> </form>
</div> </div>

@ -9,11 +9,12 @@
{{ with .Decision }} {{ with .Decision }}
<div class="column"> <div class="column">
<div class="ui raised segment"> <div class="ui raised segment">
{{ template "motion_fragment" . }} {{ template "motion_fragment" . }}
</div> </div>
</div> </div>
{{ end }} {{ end }}
<form action="/motions/{{ .Decision.Tag }}/withdraw" method="post"> <form action="/motions/{{ .Decision.Tag }}/withdraw" method="post">
{{ csrfField }}
<div class="ui form"> <div class="ui form">
<button class="ui primary left labeled icon button" type="submit"><i class="trash icon"></i> Withdraw</button> <button class="ui primary left labeled icon button" type="submit"><i class="trash icon"></i> Withdraw</button>
</div> </div>

@ -7,6 +7,7 @@ client_ca_certificates: cacert_class3.pem
server_certificate: server.crt server_certificate: server.crt
server_key: server.key server_key: server.key
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes 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 base_url: https://motions.cacert.org
migrations_path: db migrations_path: db
mail_server: mail_server:

Loading…
Cancel
Save