POSTing to a CSRF-protected page

563 views
Skip to first unread message

Nathan

unread,
Mar 22, 2017, 8:23:27 PM3/22/17
to Gorilla web toolkit
I've added CSRF protection to the login page for a web app I'm building and I'd like to test logins through Go's standard testing procedures. However, I can log in just fine in a browser but I get "Forbidden - CSRF token invalid" errors when I'm using http.Client. Can anyone give me some pointers on how I might better debug what's going on? I'm still new to using middleware.

Here's the sign-in test:

func TestSignIn(t *testing.T) {
t.Parallel()

expectationses := []expectations{
expectations{
username: "Box Box Box Space Box",
password: "asdf",
status:   http.StatusOK,
},
expectations{
username: "Box Box Box Space Box",
password: "",
status:   http.StatusForbidden,
},
expectations{
username: "",
password: "",
status:   http.StatusForbidden,
},
expectations{
username: "Nobody Great",
password: "asdf",
status:   http.StatusForbidden,
},
}

for _, ex := range expectationses {
c := http.Client{}
t.Logf("Expecting status %v for “%v”:“%v”", ex.status, ex.username, ex.password)

rr, err := c.Get(baseURL)
if err != nil {
t.Fatalf("Tried to get / to find out what the CSRF token is, but couldn’t: %v", err)
}

doc, err := html.Parse(rr.Body)
if err != nil {
t.Fatalf("Couldn’t parse /’s body: %v", err)
}

var csrfToken string

var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "input" {
for _, attr := range n.Attr {
if attr.Key == "name" && attr.Val == "gorilla.csrf.Token" {
for _, attr := range n.Attr {
if attr.Key == "value" {
csrfToken = attr.Val
}
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)

resp, err := c.PostForm(baseURL+"signin/", url.Values{
"username":           {ex.username},
"password":           {ex.password},
"gorilla.csrf.Token": {csrfToken},
})
if err != nil {
t.Fatalf("Couldn’t post to /signin/ as “%v”", ex.username)
}

if resp.StatusCode != ex.status {
body, _ := ioutil.ReadAll(resp.Body)
t.Fatalf("“%v” with password “%v” and CSRF token “%s” expected %v as a status code, but got %v instead. Body:\n%s",
ex.username, ex.password, csrfToken, ex.status, resp.StatusCode, body)
}

if resp.StatusCode == http.StatusOK {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Couldn’t read body")
}

MustSlash(t, body)
}
}
}


Here's what `go test` prints out:

--- FAIL: TestSignIn (0.01s)

blackbox_test.go:130: “Box Box Box Space Box” with password “asdf” and CSRF token “” expected 200 as a status code, but got 403 instead. Body:

Forbidden - CSRF token invalid

FAIL


I set up my handlers in main(). Here's what it looks like:

func main() {
var db *sql.DB
var err error

    // set up database

if err = CreatePlayer(db, "Box Box Box Space Box", "wal...@example.com", "asdf"); err != nil {
log.Fatal(err)
}

var secureness csrf.Option
switch runtime.GOOS {
case "linux":
secureness = csrf.Secure(true)
case "darwin":
secureness = csrf.Secure(false)
default:
log.Fatal("On neither Linux (production) nor macOS (dev). Do I use secure cookies or not?")
}

CSRF := csrf.Protect([]byte("nope nope nope nope"), secureness)

r := mux.NewRouter()
r.Handle("/", CSRF(slashHandler(db)))
r.HandleFunc("/players/me/nowwhat/", nowWhatHandler(db))
r.Handle("/signin/", CSRF(signInHandler(db)))
r.HandleFunc("/signup/", signUpHandler(db))
r.HandleFunc("/signout/", signOutHandler(db))
r.HandleFunc("/locations/{location}/", locationsHandler(db))
r.Handle("/store/{store}/", CSRF(storeHandler(db)))

r.HandleFunc("/about/", aboutHandler(db))

http.Handle("/", r)

log.Printf("Serving on a port you shouldn’t be listening to directly…")
log.Fatal(http.ListenAndServe("localhost:8888", r))
}


(No, I don't have a good handle — pardon the expression — on which handlers should be CSRF-protected. I don't think the About page needs it because it's supposed to be readable by anyone who visits the site, but / should have it (because it's a sign-in page) and probably all the other pages that are only accessed by logged-in users, right?

Matt S

unread,
Mar 22, 2017, 8:52:55 PM3/22/17
to goril...@googlegroups.com
Quick answer while commuting: you need a cookiejar to hold the csrf cookie on the subsequent request. 

--
You received this message because you are subscribed to the Google Groups "Gorilla web toolkit" group.
To unsubscribe from this group and stop receiving emails from it, send an email to gorilla-web...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Nathan

unread,
Mar 22, 2017, 8:59:42 PM3/22/17
to Gorilla web toolkit
[uncomments yesterday's cookie-jar code]

Still not sure what i'm not doing properly. Here's the with-cookie-jar code:

func TestSignIn(t *testing.T) {
t.Parallel()

expectationses := []expectations{
expectations{
username: "Box Box Box Space Box",
password: "asdf",
status:   http.StatusOK,
},
expectations{
username: "Box Box Box Space Box",
password: "",
status:   http.StatusForbidden,
},
expectations{
username: "",
password: "",
status:   http.StatusForbidden,
},
expectations{
username: "Nobody Great",
password: "asdf",
status:   http.StatusForbidden,
},
}

baseURLURL, err := url.Parse(baseURL)
if err != nil {
t.Fatal("base URL parsing messed up somehow")
}

for _, ex := range expectationses {
c := http.Client{}
t.Logf("Expecting status %v for “%v”:“%v”", ex.status, ex.username, ex.password)

rr, err := c.Get(baseURL)
if err != nil {
t.Fatalf("Tried to get / to find out what the CSRF token is, but couldn’t: %v", err)
}

doc, err := html.Parse(rr.Body)
if err != nil {
t.Fatalf("Couldn’t parse /’s body: %v", err)
}

var csrfToken string

var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "input" {
for _, attr := range n.Attr {
if attr.Key == "name" && attr.Val == "gorilla.csrf.Token" {
for _, attr := range n.Attr {
if attr.Key == "value" {
csrfToken = attr.Val
}
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)

cookie := http.Cookie{Name: "_gorilla_csrf", Value: csrfToken}
cookies := make([]*http.Cookie, 0, 1)
cookies = append(cookies, &cookie)

c.Jar, err = cookiejar.New(nil)
if err != nil {
t.Fatalf("Couldn’t make a cookie jar")
}
c.Jar.SetCookies(baseURLURL, cookies)

resp, err := c.PostForm(baseURL+"signin/", url.Values{
"username": {ex.username},
"password": {ex.password},
// "gorilla.csrf.Token": {csrfToken},
})
if err != nil {
t.Fatalf("Couldn’t post to /signin/ as “%v”", ex.username)
}

if resp.StatusCode != ex.status {
body, _ := ioutil.ReadAll(resp.Body)
t.Fatalf("“%v” with password “%v” and CSRF token “%s” expected %v as a status code, but got %v instead. Body:\n%s",
ex.username, ex.password, csrfToken, ex.status, resp.StatusCode, body)
}

if resp.StatusCode == http.StatusOK {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Couldn’t read body")
}

MustSlash(t, body)
}
}
}


And the failing test:

--- FAIL: TestSignIn (0.01s)

blackbox_test.go:78: Expecting status 200 for “Box Box Box Space Box”:“asdf”

blackbox_test.go:133: “Box Box Box Space Box” with password “asdf” and CSRF token “hLM80NknpMSANlRQAFIQ0kI2rk5fXlRw1ZftlZ717b+uOhbJRFMJIv/7dMQL9jbwuQjaAV5H0MdXEKynyXQ5eg==” expected 200 as a status code, but got 403 instead. Body:

Forbidden - CSRF token invalid

FAIL

Matt S

unread,
Mar 23, 2017, 8:54:56 AM3/23/17
to goril...@googlegroups.com
You want to be sending the cookie in the body, and POST'ing the token (the token & cookie value are not equal) in the form - you have the form code commented out?

// "gorilla.csrf.Token": {csrfToken},

--

Matt Silverlock

unread,
Mar 23, 2017, 9:25:42 AM3/23/17
to Gorilla web toolkit
You can also see how I test the lib itself here: https://github.com/gorilla/csrf/blob/master/csrf_test.go


On Thursday, March 23, 2017 at 5:54:56 AM UTC-7, Matt Silverlock wrote:
You want to be sending the cookie in the body, and POST'ing the token (the token & cookie value are not equal) in the form - you have the form code commented out?

// "gorilla.csrf.Token": {csrfToken},

Nathan

unread,
Mar 23, 2017, 5:57:21 PM3/23/17
to Gorilla web toolkit
"and". That fixed it. Thanks!

When I started all this I thought just sending the token from the form would be enough — or, alternately, just sending the cookie back would be enough just by itself. I didn't infer "you need to send the cookie back as a cookie in addition to either (1) the X-CSRF-Token header or (2) the gorilla.csrf.Token hidden form field."

I don't expect there'll be too many people who're new to CSRF protection who want to do this sort of thing programmatically, but would it be a good idea to insert something like the following before "In addition: getting CSRF protection right is important, so here's some background:"?

> If you're writing a client that's supposed to mimic browser behavior, make sure to send back the CSRF cookie (the default name is _gorilla_csrf, but this can be changed with the CookieName Option) along with either the X-CSRF-Token header or the gorilla.csrf.Token form field.

Thanks again,
    Nathan


On Thursday, March 23, 2017 at 5:54:56 AM UTC-7, Matt Silverlock wrote:

Matt S

unread,
Mar 23, 2017, 9:56:05 PM3/23/17
to goril...@googlegroups.com
Would you like to submit a PR to the README? (Super busy right now but would appreciate it)

--
Reply all
Reply to author
Forward
0 new messages