implement XSRF/CSRF protection for /stop and /restart
While stopping/restarting processes seems like a nuisance at best, it’s good style to prevent these attacks. This commit the Double-Submit Cookie protection, which seems appropriate for our architecture and demands. See also: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Double_Submit_Cookie
This commit is contained in:
parent
7dc7a36757
commit
c74265523b
@ -11,7 +11,17 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="#{{ .Service.Name }}">{{ .Service.Name }}</a></td>
|
<td><a href="#{{ .Service.Name }}">{{ .Service.Name }}</a></td>
|
||||||
<td>{{ .Service.Started }}</td>
|
<td>{{ .Service.Started }}</td>
|
||||||
<td><form method="POST" action="/restart"><input type="hidden" name="path" value="{{ .Service.Name }}"><input type="submit" value="restart"></form><form method="POST" action="/stop"><input type="hidden" name="path" value="{{ .Service.Name }}"><input type="submit" value="stop"></form></td>
|
<td>
|
||||||
|
<form method="POST" action="/restart">
|
||||||
|
<input type="hidden" name="xsrftoken" value="{{ .XsrfToken }}">
|
||||||
|
<input type="hidden" name="path" value="{{ .Service.Name }}">
|
||||||
|
<input type="submit" value="restart">
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/stop">
|
||||||
|
<input type="hidden" name="xsrftoken" value="{{ .XsrfToken }}">
|
||||||
|
<input type="hidden" name="path" value="{{ .Service.Name }}">
|
||||||
|
<input type="submit" value="stop">
|
||||||
|
</form></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -10,4 +10,4 @@ var assets = map[string][]byte{
|
|||||||
var assets_0 = []byte("<!DOCTYPE html>\n<html lang=\"en\">\n<title>{{ .Hostname }} — gokrazy</title>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=\" crossorigin=\"anonymous\" />\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css\" integrity=\"sha256-eU4xmpfQx1HSi5q1q2rHNcMEzTNJov7r2Wr/6zF3ANc=\" crossorigin=\"anonymous\" />\n<style type=\"text/css\">\n.progress-bar:nth-child(5n) {\n background-color: #337ab7;\n}\n.progress-bar:nth-child(5n+1) {\n background-color: #5cb85c;\n}\n.progress-bar:nth-child(5n+2) {\n background-color: #5bc0de;\n}\n.progress-bar:nth-child(5n+3) {\n background-color: #f0ad4e;\n}\n.progress-bar:nth-child(5n+4) {\n background-color: #d9534f;\n}\n.lastlog {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\ntable {\n table-layout: fixed;\n}\n</style>\n\n <nav class=\"navbar navbar-default\">\n <div class=\"container-fluid\">\n <!-- Brand and toggle get grouped for better mobile display -->\n <div class=\"navbar-header\">\n <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#navbar-collapse-1\" aria-expanded=\"false\">\n <span class=\"sr-only\">Toggle navigation</span>\n <span class=\"icon-bar\"></span>\n <span class=\"icon-bar\"></span>\n <span class=\"icon-bar\"></span>\n </button>\n <p style=\"width: 50ex; margin-top: 0.25em; font-size: 18px\"><a href=\"/\">gokrazy</a><br>\n <small style=\"font-size: 11px\" class=\"text-muted\">version {{ .BuildTimestamp }}</small></p>\n </div>\n\n <div class=\"collapse navbar-collapse\" id=\"navbar-collapse-1\">\n <ul class=\"nav navbar-nav\">\n </ul>\n\n <p class=\"navbar-text navbar-right\">\n host “{{ .Hostname }}”\n </p>\n </div><!-- /.navbar-collapse -->\n </div><!-- /.container-fluid -->\n </nav>\n\n <div class=\"container\">\n")
|
var assets_0 = []byte("<!DOCTYPE html>\n<html lang=\"en\">\n<title>{{ .Hostname }} — gokrazy</title>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=\" crossorigin=\"anonymous\" />\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css\" integrity=\"sha256-eU4xmpfQx1HSi5q1q2rHNcMEzTNJov7r2Wr/6zF3ANc=\" crossorigin=\"anonymous\" />\n<style type=\"text/css\">\n.progress-bar:nth-child(5n) {\n background-color: #337ab7;\n}\n.progress-bar:nth-child(5n+1) {\n background-color: #5cb85c;\n}\n.progress-bar:nth-child(5n+2) {\n background-color: #5bc0de;\n}\n.progress-bar:nth-child(5n+3) {\n background-color: #f0ad4e;\n}\n.progress-bar:nth-child(5n+4) {\n background-color: #d9534f;\n}\n.lastlog {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\ntable {\n table-layout: fixed;\n}\n</style>\n\n <nav class=\"navbar navbar-default\">\n <div class=\"container-fluid\">\n <!-- Brand and toggle get grouped for better mobile display -->\n <div class=\"navbar-header\">\n <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#navbar-collapse-1\" aria-expanded=\"false\">\n <span class=\"sr-only\">Toggle navigation</span>\n <span class=\"icon-bar\"></span>\n <span class=\"icon-bar\"></span>\n <span class=\"icon-bar\"></span>\n </button>\n <p style=\"width: 50ex; margin-top: 0.25em; font-size: 18px\"><a href=\"/\">gokrazy</a><br>\n <small style=\"font-size: 11px\" class=\"text-muted\">version {{ .BuildTimestamp }}</small></p>\n </div>\n\n <div class=\"collapse navbar-collapse\" id=\"navbar-collapse-1\">\n <ul class=\"nav navbar-nav\">\n </ul>\n\n <p class=\"navbar-text navbar-right\">\n host “{{ .Hostname }}”\n </p>\n </div><!-- /.navbar-collapse -->\n </div><!-- /.container-fluid -->\n </nav>\n\n <div class=\"container\">\n")
|
||||||
var assets_1 = []byte("\n</div>\n\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js\" integrity=\"sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js\" integrity=\"sha256-eXHLyyVI+v6X1wbfg9NB05IWqOqY4E9185nHZgeDIhg=\" crossorigin=\"anonymous\"></script>\n\n</html>")
|
var assets_1 = []byte("\n</div>\n\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js\" integrity=\"sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js\" integrity=\"sha256-eXHLyyVI+v6X1wbfg9NB05IWqOqY4E9185nHZgeDIhg=\" crossorigin=\"anonymous\"></script>\n\n</html>")
|
||||||
var assets_2 = []byte("{{ template \"header\" . }}\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n\n<h1>services</h1>\n\n<table class=\"table\">\n<tbody><tr>\n<th width=\"20%\">path</th>\n<th width=\"80%\">last log line</th>\n</tr>\n\n{{ range $idx, $svc := .Services }}\n<tr>\n<td>\n<a href=\"/status?path={{ $svc.Name }}\">{{ $svc.Name }}</a>\n{{ if restarting $svc.Started }}\n<span class=\"label label-danger\">restarting</span>\n{{ end }}\n{{ if $svc.Stopped }}\n<span class=\"label label-warning\">stopped</span>\n{{ end }}\n</td>\n<td class=\"lastlog\">\n{{ last $svc.Stdout.Lines $svc.Stderr.Lines }}\n</td>\n</tr>\n{{ end }}\n\n</table>\n</div> \n<div class=\"col-md-12\">\n<h2>memory</h2>\n{{ megabytes (index .Meminfo \"MemTotal\") }} total, {{ megabytes (index .Meminfo \"MemAvailable\") }} available<br>\n<strong>resident set size (RSS) by service</strong>:\n<div class=\"progress\">\n\n{{ with $rss := initRss }}\n<div class=\"progress-bar\" style=\"width: {{ rssPercentage $.Meminfo $rss }}%\" title=\"init uses {{ megabytes $rss }} RSS\">\n<span class=\"sr-only\"></span>\ninit\n</div>\n{{ end }}\n\n{{ range $idx, $svc := .Services }}\n{{ with $rss := $svc.RSS }}\n<div class=\"progress-bar\" style=\"width: {{ rssPercentage $.Meminfo $rss }}%\" title=\"{{ $svc.Name }} uses {{ megabytes $rss }} RSS\">\n<span class=\"sr-only\"></span>\n{{ baseName $svc.Name }}\n</div>\n{{ end }}\n{{ end }}\n<div class=\"progress-bar\" style=\"width: 100%; overflow:initial; float: none\" title=\"memory usage outside of gokrazy services\">\n<span class=\"sr-only\"></span>\nunaccounted\n</div>\n</div>\n</div>\n\n<div class=\"col-md-12\">\n\n\n<h2>storage</h2>\n\n{{ if eq .PermAvail 0 }}\nNo permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and, if your SD card is <code>/dev/sdb</code>, use <code>mkfs.ext4 /dev/sdb4</code>.\n{{ else }}\n<strong>{{ .PermDev }}</strong>: {{ gigabytes .PermTotal }} total, {{ gigabytes .PermUsed }} used, {{ gigabytes .PermAvail }} avail<br>\n{{ end }}\n\n<h2>private network addresses</h2>\n<ul>\n{{ range $idx, $addr := .PrivateAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n<h2>public network addresses</h2>\n<ul>\n{{ range $idx, $addr := .PublicAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n\n</div>\n</div>\n\n{{ template \"footer\" . }}\n")
|
var assets_2 = []byte("{{ template \"header\" . }}\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n\n<h1>services</h1>\n\n<table class=\"table\">\n<tbody><tr>\n<th width=\"20%\">path</th>\n<th width=\"80%\">last log line</th>\n</tr>\n\n{{ range $idx, $svc := .Services }}\n<tr>\n<td>\n<a href=\"/status?path={{ $svc.Name }}\">{{ $svc.Name }}</a>\n{{ if restarting $svc.Started }}\n<span class=\"label label-danger\">restarting</span>\n{{ end }}\n{{ if $svc.Stopped }}\n<span class=\"label label-warning\">stopped</span>\n{{ end }}\n</td>\n<td class=\"lastlog\">\n{{ last $svc.Stdout.Lines $svc.Stderr.Lines }}\n</td>\n</tr>\n{{ end }}\n\n</table>\n</div> \n<div class=\"col-md-12\">\n<h2>memory</h2>\n{{ megabytes (index .Meminfo \"MemTotal\") }} total, {{ megabytes (index .Meminfo \"MemAvailable\") }} available<br>\n<strong>resident set size (RSS) by service</strong>:\n<div class=\"progress\">\n\n{{ with $rss := initRss }}\n<div class=\"progress-bar\" style=\"width: {{ rssPercentage $.Meminfo $rss }}%\" title=\"init uses {{ megabytes $rss }} RSS\">\n<span class=\"sr-only\"></span>\ninit\n</div>\n{{ end }}\n\n{{ range $idx, $svc := .Services }}\n{{ with $rss := $svc.RSS }}\n<div class=\"progress-bar\" style=\"width: {{ rssPercentage $.Meminfo $rss }}%\" title=\"{{ $svc.Name }} uses {{ megabytes $rss }} RSS\">\n<span class=\"sr-only\"></span>\n{{ baseName $svc.Name }}\n</div>\n{{ end }}\n{{ end }}\n<div class=\"progress-bar\" style=\"width: 100%; overflow:initial; float: none\" title=\"memory usage outside of gokrazy services\">\n<span class=\"sr-only\"></span>\nunaccounted\n</div>\n</div>\n</div>\n\n<div class=\"col-md-12\">\n\n\n<h2>storage</h2>\n\n{{ if eq .PermAvail 0 }}\nNo permanent storage mounted. To create a filesystem for permanent storage, plug the SD card into a Linux computer and, if your SD card is <code>/dev/sdb</code>, use <code>mkfs.ext4 /dev/sdb4</code>.\n{{ else }}\n<strong>{{ .PermDev }}</strong>: {{ gigabytes .PermTotal }} total, {{ gigabytes .PermUsed }} used, {{ gigabytes .PermAvail }} avail<br>\n{{ end }}\n\n<h2>private network addresses</h2>\n<ul>\n{{ range $idx, $addr := .PrivateAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n<h2>public network addresses</h2>\n<ul>\n{{ range $idx, $addr := .PublicAddrs }}\n<li>{{ $addr }}</li>\n{{ end }}\n</ul>\n\n\n</div>\n</div>\n\n{{ template \"footer\" . }}\n")
|
||||||
var assets_3 = []byte("{{ template \"header\" . }}\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n<table>\n<tr>\n<th>Name</th>\n<th>Started</th>\n<th>Actions</th>\n</tr>\n<tr>\n<td><a href=\"#{{ .Service.Name }}\">{{ .Service.Name }}</a></td>\n<td>{{ .Service.Started }}</td>\n<td><form method=\"POST\" action=\"/restart\"><input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\"><input type=\"submit\" value=\"restart\"></form><form method=\"POST\" action=\"/stop\"><input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\"><input type=\"submit\" value=\"stop\"></form></td>\n</tr>\n</table>\n\n <h3>stdout</h3>\n <pre>\n {{ range $idx, $line := .Service.Stdout.Lines -}}\n {{ $line }}\n {{ end }}\n </pre>\n\n <h3>stderr</h3>\n <pre>\n {{ range $idx, $line := .Service.Stderr.Lines -}}\n {{ $line }}\n {{ end }}\n </pre>\n</div>\n</div>\n\n{{ template \"footer\" . }}")
|
var assets_3 = []byte("{{ template \"header\" . }}\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n<table>\n<tr>\n<th>Name</th>\n<th>Started</th>\n<th>Actions</th>\n</tr>\n<tr>\n<td><a href=\"#{{ .Service.Name }}\">{{ .Service.Name }}</a></td>\n<td>{{ .Service.Started }}</td>\n<td>\n <form method=\"POST\" action=\"/restart\">\n <input type=\"hidden\" name=\"xsrftoken\" value=\"{{ .XsrfToken }}\">\n <input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\">\n <input type=\"submit\" value=\"restart\">\n </form>\n <form method=\"POST\" action=\"/stop\">\n <input type=\"hidden\" name=\"xsrftoken\" value=\"{{ .XsrfToken }}\">\n <input type=\"hidden\" name=\"path\" value=\"{{ .Service.Name }}\">\n <input type=\"submit\" value=\"stop\">\n </form></td>\n</tr>\n</table>\n\n <h3>stdout</h3>\n <pre>\n {{ range $idx, $line := .Service.Stdout.Lines -}}\n {{ $line }}\n {{ end }}\n </pre>\n\n <h3>stderr</h3>\n <pre>\n {{ range $idx, $line := .Service.Stderr.Lines -}}\n {{ $line }}\n {{ end }}\n </pre>\n</div>\n</div>\n\n{{ template \"footer\" . }}\n")
|
||||||
|
28
status.go
28
status.go
@ -94,24 +94,38 @@ var statusTmpl = template.Must(template.Must(commonTmpls.Clone()).New("statusTmp
|
|||||||
|
|
||||||
func initStatus(services []*service) {
|
func initStatus(services []*service) {
|
||||||
http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.FormValue("path")
|
token := xsrfTokenFromCookies(r.Cookies())
|
||||||
var svc *service
|
if token == 0 {
|
||||||
for _, s := range services {
|
// Only generate a new XSRF token if the old one is expired, so that
|
||||||
if s.cmd.Path != path {
|
// loading a different form in the background doesn’t render the
|
||||||
continue
|
// current one unusable.
|
||||||
|
token = xsrfToken()
|
||||||
}
|
}
|
||||||
svc = s
|
|
||||||
break
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "gokrazy_xsrf",
|
||||||
|
Value: fmt.Sprintf("%d", token),
|
||||||
|
Expires: time.Now().Add(24 * time.Hour),
|
||||||
|
HttpOnly: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
path := r.FormValue("path")
|
||||||
|
svc := findSvc(path)
|
||||||
|
if svc == nil {
|
||||||
|
http.Error(w, "service not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := statusTmpl.Execute(&buf, struct {
|
if err := statusTmpl.Execute(&buf, struct {
|
||||||
Service *service
|
Service *service
|
||||||
BuildTimestamp string
|
BuildTimestamp string
|
||||||
Hostname string
|
Hostname string
|
||||||
|
XsrfToken int32
|
||||||
}{
|
}{
|
||||||
Service: svc,
|
Service: svc,
|
||||||
BuildTimestamp: buildTimestamp,
|
BuildTimestamp: buildTimestamp,
|
||||||
Hostname: hostname,
|
Hostname: hostname,
|
||||||
|
XsrfToken: token,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
17
supervise.go
17
supervise.go
@ -255,6 +255,21 @@ func stopstartHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cookieToken := xsrfTokenFromCookies(r.Cookies())
|
||||||
|
if cookieToken == 0 {
|
||||||
|
http.Error(w, "XSRF cookie missing", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i, err := strconv.ParseInt(r.FormValue("xsrftoken"), 0, 32)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("parsing XSRF token form value: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if formToken := int32(i); cookieToken != formToken {
|
||||||
|
http.Error(w, "XSRF token mismatch", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
signal := syscall.SIGTERM
|
signal := syscall.SIGTERM
|
||||||
if r.FormValue("signal") == "kill" {
|
if r.FormValue("signal") == "kill" {
|
||||||
signal = syscall.SIGKILL
|
signal = syscall.SIGKILL
|
||||||
@ -266,7 +281,7 @@ func stopstartHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "no such service", http.StatusNotFound)
|
http.Error(w, "no such service", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
if r.URL.Path == "/restart" {
|
if r.URL.Path == "/restart" {
|
||||||
err = restart(s, signal)
|
err = restart(s, signal)
|
||||||
} else {
|
} else {
|
||||||
|
42
xsrf.go
Normal file
42
xsrf.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package gokrazy
|
||||||
|
|
||||||
|
import (
|
||||||
|
cryptorand "crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func xsrfTokenFromCookies(cookies []*http.Cookie) int32 {
|
||||||
|
for _, c := range cookies {
|
||||||
|
if c.Name != "gokrazy_xsrf" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i, err := strconv.ParseInt(c.Value, 0, 32); err == nil {
|
||||||
|
return int32(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazyXsrf is a lazily initialized source of random numbers for generating XSRF
|
||||||
|
// tokens. It is lazily initialized to not block early boot when reading
|
||||||
|
// cryptographically strong random bytes to seed the RNG.
|
||||||
|
var lazyXsrf struct {
|
||||||
|
once sync.Once
|
||||||
|
rnd *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
func xsrfToken() int32 {
|
||||||
|
lazyXsrf.once.Do(func() {
|
||||||
|
var buf [8]byte
|
||||||
|
if _, err := cryptorand.Read(buf[:]); err != nil {
|
||||||
|
log.Fatalf("lazyXsrf: cryptorand.Read: %v", err)
|
||||||
|
}
|
||||||
|
lazyXsrf.rnd = rand.New(rand.NewSource(int64(binary.BigEndian.Uint64(buf[:]))))
|
||||||
|
})
|
||||||
|
return lazyXsrf.rnd.Int31()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user