Skip to content

Commit

Permalink
Add CSRF protection to deck
Browse files Browse the repository at this point in the history
  • Loading branch information
mirandachrist committed Jul 8, 2019
1 parent 64c4e48 commit 76bb84e
Show file tree
Hide file tree
Showing 22 changed files with 1,045 additions and 7 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ require (
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/googleapis/gnostic v0.1.0 // indirect
github.com/gophercloud/gophercloud v0.0.0-20181215224939-bdd8b1ecd793 // indirect
github.com/gorilla/csrf v1.6.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.1.3
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ github.com/gophercloud/gophercloud v0.0.0-20181215224939-bdd8b1ecd793 h1:rT82/6k
github.com/gophercloud/gophercloud v0.0.0-20181215224939-bdd8b1ecd793/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.6.0 h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
Expand Down
1 change: 1 addition & 0 deletions prow/cmd/deck/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ go_library(
"//prow/tide/history:go_default_library",
"//vendor/cloud.google.com/go/storage:go_default_library",
"//vendor/github.com/NYTimes/gziphandler:go_default_library",
"//vendor/github.com/gorilla/csrf:go_default_library",
"//vendor/github.com/gorilla/sessions:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/github.com/sirupsen/logrus:go_default_library",
Expand Down
34 changes: 30 additions & 4 deletions prow/cmd/deck/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (

"cloud.google.com/go/storage"
"github.com/NYTimes/gziphandler"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -93,6 +94,7 @@ type options struct {
spyglassFilesLocation string
gcsCredentialsFile string
rerunCreatesJob bool
csrfTokenFile string
}

func (o *options) Validate() error {
Expand Down Expand Up @@ -139,6 +141,8 @@ func gatherOptions(fs *flag.FlagSet, args ...string) options {
fs.StringVar(&o.templateFilesLocation, "template-files-location", "/template", "Path to the template files")
fs.StringVar(&o.gcsCredentialsFile, "gcs-credentials-file", "", "Path to the GCS credentials file")
fs.BoolVar(&o.rerunCreatesJob, "rerun-creates-job", false, "Change the re-run option in Deck to actually create the job. **WARNING:** Only use this with non-public deck instances, otherwise strangers can DOS your Prow instance")
// csrfTokenFile, if specified, must point to a file containing a 32 byte token that will be used to protect against CSRF in POST requests
fs.StringVar(&o.csrfTokenFile, "csrf-token", "/etc/token/secret", "Path to the file containing the CSRF token.")
o.kubernetes.AddFlags(fs)
fs.Parse(args)
o.configPath = config.ConfigPath(o.configPath)
Expand Down Expand Up @@ -272,8 +276,6 @@ func main() {
mux.Handle("/tide-history", gziphandler.GzipHandler(handleSimpleTemplate(o, cfg, "tide-history.html", nil)))
mux.Handle("/plugins", gziphandler.GzipHandler(handleSimpleTemplate(o, cfg, "plugins.html", nil)))

indexHandler := handleSimpleTemplate(o, cfg, "index.html", struct{ SpyglassEnabled, ReRunCreatesJob bool }{o.spyglass, o.rerunCreatesJob})

runLocal := o.pregeneratedData != ""

var fallbackHandler func(http.ResponseWriter, *http.Request)
Expand All @@ -289,6 +291,11 @@ func main() {
fallbackHandler(w, r)
return
}
indexHandler := handleSimpleTemplate(o, cfg, "index.html", struct {
SpyglassEnabled bool
ReRunCreatesJob bool
CSRFToken string
}{o.spyglass, o.rerunCreatesJob, csrf.Token(r)})
indexHandler(w, r)
})

Expand All @@ -301,8 +308,24 @@ func main() {
// signal to the world that we're ready
health.ServeReady()

if o.rerunCreatesJob && o.csrfTokenFile == "" {
logrus.Warning("Allowing direct reruns is susceptible to CSRF attacks. We will soon be requiring you to specify a file containing a CSRF token if --rerun-creates-job is enabled.")
}

// if a CSRF token is specified, we protect against CSRF in all post requests
if o.csrfTokenFile != "" {
csrfToken, err := loadToken(o.csrfTokenFile)
if err != nil {
logrus.WithError(err).Fatal("Could not retrieve CSRF token.")
}
// in development, pass in csrf.Secure(false)
CSRF := csrf.Protect(csrfToken, csrf.Path("/"))
logrus.WithError(http.ListenAndServe(":8080", CSRF(traceHandler(mux)))).Fatal("ListenAndServe returned.")
return
}
// setup done, actually start the server
logrus.WithError(http.ListenAndServe(":8080", traceHandler(mux))).Fatal("ListenAndServe returned.")

}

// localOnlyMain contains logic used only when running locally, and is mutually exclusive with
Expand Down Expand Up @@ -724,7 +747,8 @@ func handleRequestJobViews(sg *spyglass.Spyglass, cfg config.Getter, o options)
setHeadersNoCaching(w)
src := strings.TrimPrefix(r.URL.Path, "/view/")

page, err := renderSpyglass(sg, cfg, src, o)
csrfToken := csrf.Token(r)
page, err := renderSpyglass(sg, cfg, src, o, csrfToken)
if err != nil {
logrus.WithError(err).Error("error rendering spyglass page")
message := fmt.Sprintf("error rendering spyglass page: %v", err)
Expand All @@ -743,7 +767,7 @@ func handleRequestJobViews(sg *spyglass.Spyglass, cfg config.Getter, o options)
}

// renderSpyglass returns a pre-rendered Spyglass page from the given source string
func renderSpyglass(sg *spyglass.Spyglass, cfg config.Getter, src string, o options) (string, error) {
func renderSpyglass(sg *spyglass.Spyglass, cfg config.Getter, src string, o options, csrfToken string) (string, error) {
renderStart := time.Now()

src = strings.TrimSuffix(src, "/")
Expand Down Expand Up @@ -893,6 +917,7 @@ lensesLoop:
TestgridLink string
JobName string
BuildID string
CSRFToken string
ExtraLinks []spyglass.ExtraLink
}
lTmpl := lensesTemplate{
Expand All @@ -908,6 +933,7 @@ lensesLoop:
TestgridLink: tgLink,
JobName: jobName,
BuildID: buildID,
CSRFToken: csrfToken,
ExtraLinks: extraLinks,
}
t := template.New("spyglass.html")
Expand Down
2 changes: 2 additions & 0 deletions prow/cmd/deck/static/pr/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {getCookieByName, tidehistory} from '../common/common';

declare const tideData: TideData;
declare const allBuilds: Job[];
declare const csrfToken: string;

type UnifiedState = JobState | "expected" | "error" | "failure" | "pending" | "success";

Expand Down Expand Up @@ -115,6 +116,7 @@ function createXMLHTTPRequest(fulfillFn: (request: XMLHttpRequest) => any, error
request.withCredentials = true;
request.open("POST", url, true);
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
request.setRequestHeader("X-CSRF-Token", csrfToken);

return request;
}
Expand Down
6 changes: 6 additions & 0 deletions prow/cmd/deck/static/prow/prow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {JobHistogram, JobSample} from './histogram';
declare const allBuilds: Job[];
declare const spyglass: boolean;
declare const rerunCreatesJob: boolean;
declare const csrfToken: string;

// http://stackoverflow.com/a/5158301/3694
function getParameterByName(name: string): string | null {
Expand Down Expand Up @@ -686,6 +687,11 @@ function createRerunCell(modal: HTMLElement, rerunElement: HTMLElement, prowjob:
const form = document.createElement('form');
form.method = 'POST';
form.action = `${url}`;
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'gorilla.csrf.Token';
tokenInput.value = csrfToken;
form.append(tokenInput);
c.appendChild(form);
form.submit();
};
Expand Down
7 changes: 4 additions & 3 deletions prow/cmd/deck/static/spyglass/spyglass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isTransitMessage } from "./common";
declare const src: string;
declare const lensArtifacts: {[key: string]: string[]};
declare const lensIndexes: number[];
declare const csrfToken: string;

// Loads views for this job
function loadLenses(): void {
Expand Down Expand Up @@ -58,13 +59,13 @@ window.addEventListener('message', async (e) => {
break;
case "request": {
const req = await fetch(urlForLensRequest(lens, index, 'callback'),
{body: message.data, method: 'POST'});
{body: message.data, method: 'POST', headers: {'X-CSRF-Token': csrfToken}});
respond(await req.text());
break;
}
case "requestPage": {
const req = await fetch(urlForLensRequest(lens, index, 'rerender'),
{body: message.data, method: 'POST'});
{body: message.data, method: 'POST', headers: {'X-CSRF-Token': csrfToken}});
respond(await req.text());
break;
}
Expand All @@ -73,7 +74,7 @@ window.addEventListener('message', async (e) => {
frame.style.visibility = 'visible';
spinner.style.display = 'block';
const req = await fetch(urlForLensRequest(lens, index, 'rerender'),
{body: message.data, method: 'POST'});
{body: message.data, method: 'POST', headers: {'X-CSRF-Token': csrfToken}});
respond(await req.text());
break;
}
Expand Down
1 change: 1 addition & 0 deletions prow/cmd/deck/template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<script type="text/javascript">
var spyglass = {{.SpyglassEnabled}};
var rerunCreatesJob = {{.ReRunCreatesJob}};
var csrfToken = {{.CSRFToken}};
</script>
{{end}}

Expand Down
3 changes: 3 additions & 0 deletions prow/cmd/deck/template/pr.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<script type="text/javascript" src="/static/pr_bundle.min.js"></script>
<script type="text/javascript" src="data.js?var=allBuilds"></script>
<script type="text/javascript" src="tide.js?var=tideData"></script>
<script type="text/javascript">
var csrfToken = {{.CSRFToken}};
</script>
{{end}}
{{define "content"}}
<div id="pr-container">
Expand Down
1 change: 1 addition & 0 deletions prow/cmd/deck/template/spyglass.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
var src = {{.Source}};
var lensArtifacts = {{.LensArtifacts}};
var lensIndexes = {{.LensIndexes}};
var csrfToken = {{.CSRFToken}};
</script>
<script type="text/javascript" src="/static/spyglass_bundle.min.js"></script>
<link rel="stylesheet" type="text/css" href="/static/spyglass/spyglass.css">
Expand Down
8 changes: 8 additions & 0 deletions prow/cmd/deck/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"github.com/gorilla/csrf"
"github.com/sirupsen/logrus"
"html/template"
"k8s.io/test-infra/prow/cmd/deck/version"
Expand Down Expand Up @@ -88,6 +89,13 @@ func handleSimpleTemplate(o options, cfg config.Getter, templateName string, par
http.Error(w, "error parsing template", http.StatusInternalServerError)
return
}

// ensures that the CSRF token is always passed to the template. If CSRF protection is not
// set up, the token will be an empty string.
if param == nil {
param = struct{ CSRFToken string }{csrf.Token(r)}
}

if err := t.Execute(w, param); err != nil {
logrus.WithError(err).Error("error executing template " + templateName)
http.Error(w, "error executing template", http.StatusInternalServerError)
Expand Down
6 changes: 6 additions & 0 deletions repos.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -1389,3 +1389,9 @@ def go_repositories():
sum = "h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=",
version = "v1.9.1",
)
go_repository(
name = "com_github_gorilla_csrf",
importpath = "github.com/gorilla/csrf",
sum = "h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0=",
version = "v1.6.0",
)
1 change: 1 addition & 0 deletions vendor/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ filegroup(
"//vendor/github.com/googleapis/gnostic/extensions:all-srcs",
"//vendor/github.com/gophercloud/gophercloud:all-srcs",
"//vendor/github.com/gorilla/context:all-srcs",
"//vendor/github.com/gorilla/csrf:all-srcs",
"//vendor/github.com/gorilla/mux:all-srcs",
"//vendor/github.com/gorilla/securecookie:all-srcs",
"//vendor/github.com/gorilla/sessions:all-srcs",
Expand Down
20 changes: 20 additions & 0 deletions vendor/github.com/gorilla/csrf/AUTHORS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions vendor/github.com/gorilla/csrf/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions vendor/github.com/gorilla/csrf/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions vendor/github.com/gorilla/csrf/context.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 76bb84e

Please sign in to comment.