Implement CSRF protection
This commit is contained in:
parent
33f75bdf1d
commit
5977eb5a7a
7 changed files with 88 additions and 44 deletions
14
Gopkg.lock
generated
14
Gopkg.lock
generated
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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" . }}
|
|
@ -10,20 +10,21 @@
|
|||
</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 }}">
|
||||
|
@ -39,14 +40,15 @@
|
|||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue