Skip to content
This repository was archived by the owner on Jan 24, 2019. It is now read-only.

Commit 6fb84e5

Browse files
Colin Arnottjehiah
authored andcommitted
csrf protection; always set state
1 parent 86d0832 commit 6fb84e5

File tree

3 files changed

+102
-47
lines changed

3 files changed

+102
-47
lines changed

cookie/nonce.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package cookie
2+
3+
import (
4+
"crypto/rand"
5+
"fmt"
6+
)
7+
8+
func Nonce() (nonce string, err error) {
9+
b := make([]byte, 16)
10+
_, err = rand.Read(b)
11+
if err != nil {
12+
return
13+
}
14+
nonce = fmt.Sprintf("%x", b)
15+
return
16+
}

oauthproxy.go

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@ var SignatureHeaders []string = []string{
3535
}
3636

3737
type OAuthProxy struct {
38-
CookieSeed string
39-
CookieName string
40-
CookieDomain string
41-
CookieSecure bool
42-
CookieHttpOnly bool
43-
CookieExpire time.Duration
44-
CookieRefresh time.Duration
45-
Validator func(string) bool
38+
CookieSeed string
39+
CookieName string
40+
CSRFCookieName string
41+
SessionCookieName string
42+
CookieDomain string
43+
CookieSecure bool
44+
CookieHttpOnly bool
45+
CookieExpire time.Duration
46+
CookieRefresh time.Duration
47+
Validator func(string) bool
4648

4749
RobotsPath string
4850
PingPath string
@@ -172,14 +174,16 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
172174
}
173175

174176
return &OAuthProxy{
175-
CookieName: opts.CookieName,
176-
CookieSeed: opts.CookieSecret,
177-
CookieDomain: opts.CookieDomain,
178-
CookieSecure: opts.CookieSecure,
179-
CookieHttpOnly: opts.CookieHttpOnly,
180-
CookieExpire: opts.CookieExpire,
181-
CookieRefresh: opts.CookieRefresh,
182-
Validator: validator,
177+
CookieName: opts.CookieName,
178+
CSRFCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "csrf"),
179+
SessionCookieName: fmt.Sprintf("%v_%v", opts.CookieName, "session"),
180+
CookieSeed: opts.CookieSecret,
181+
CookieDomain: opts.CookieDomain,
182+
CookieSecure: opts.CookieSecure,
183+
CookieHttpOnly: opts.CookieHttpOnly,
184+
CookieExpire: opts.CookieExpire,
185+
CookieRefresh: opts.CookieRefresh,
186+
Validator: validator,
183187

184188
RobotsPath: "/robots.txt",
185189
PingPath: "/ping",
@@ -243,7 +247,18 @@ func (p *OAuthProxy) redeemCode(host, code string) (s *providers.SessionState, e
243247
return
244248
}
245249

246-
func (p *OAuthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
250+
func (p *OAuthProxy) MakeSessionCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
251+
if value != "" {
252+
value = cookie.SignedValue(p.CookieSeed, p.SessionCookieName, value, now)
253+
}
254+
return p.makeCookie(req, p.SessionCookieName, value, expiration, now)
255+
}
256+
257+
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
258+
return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
259+
}
260+
261+
func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
247262
domain := req.Host
248263
if h, _, err := net.SplitHostPort(domain); err == nil {
249264
domain = h
@@ -255,11 +270,8 @@ func (p *OAuthProxy) MakeCookie(req *http.Request, value string, expiration time
255270
domain = p.CookieDomain
256271
}
257272

258-
if value != "" {
259-
value = cookie.SignedValue(p.CookieSeed, p.CookieName, value, now)
260-
}
261273
return &http.Cookie{
262-
Name: p.CookieName,
274+
Name: name,
263275
Value: value,
264276
Path: "/",
265277
Domain: domain,
@@ -269,20 +281,28 @@ func (p *OAuthProxy) MakeCookie(req *http.Request, value string, expiration time
269281
}
270282
}
271283

272-
func (p *OAuthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
273-
http.SetCookie(rw, p.MakeCookie(req, "", time.Hour*-1, time.Now()))
284+
func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
285+
http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
274286
}
275287

276-
func (p *OAuthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
277-
http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire, time.Now()))
288+
func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
289+
http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
290+
}
291+
292+
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) {
293+
http.SetCookie(rw, p.MakeSessionCookie(req, "", time.Hour*-1, time.Now()))
294+
}
295+
296+
func (p *OAuthProxy) SetSessionCookie(rw http.ResponseWriter, req *http.Request, val string) {
297+
http.SetCookie(rw, p.MakeSessionCookie(req, val, p.CookieExpire, time.Now()))
278298
}
279299

280300
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*providers.SessionState, time.Duration, error) {
281301
var age time.Duration
282-
c, err := req.Cookie(p.CookieName)
302+
c, err := req.Cookie(p.SessionCookieName)
283303
if err != nil {
284304
// always http.ErrNoCookie
285-
return nil, age, fmt.Errorf("Cookie %q not present", p.CookieName)
305+
return nil, age, fmt.Errorf("Cookie %q not present", p.SessionCookieName)
286306
}
287307
val, timestamp, ok := cookie.Validate(c, p.CookieSeed, p.CookieExpire)
288308
if !ok {
@@ -303,7 +323,7 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *p
303323
if err != nil {
304324
return err
305325
}
306-
p.SetCookie(rw, req, value)
326+
p.SetSessionCookie(rw, req, value)
307327
return nil
308328
}
309329

@@ -333,7 +353,7 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
333353
}
334354

335355
func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
336-
p.ClearCookie(rw, req)
356+
p.ClearSessionCookie(rw, req)
337357
rw.WriteHeader(code)
338358

339359
redirect_url := req.URL.RequestURI()
@@ -378,20 +398,18 @@ func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (st
378398
return "", false
379399
}
380400

381-
func (p *OAuthProxy) GetRedirect(req *http.Request) (string, error) {
382-
err := req.ParseForm()
383-
401+
func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
402+
err = req.ParseForm()
384403
if err != nil {
385-
return "", err
404+
return
386405
}
387406

388-
redirect := req.FormValue("rd")
389-
390-
if redirect == "" {
407+
redirect = req.Form.Get("rd")
408+
if redirect == "" || !strings.HasPrefix(redirect, "/") || strings.HasPrefix(redirect, "//") {
391409
redirect = "/"
392410
}
393411

394-
return redirect, err
412+
return
395413
}
396414

397415
func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
@@ -453,18 +471,24 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
453471
}
454472

455473
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
456-
p.ClearCookie(rw, req)
474+
p.ClearSessionCookie(rw, req)
457475
http.Redirect(rw, req, "/", 302)
458476
}
459477

460478
func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
479+
nonce, err := cookie.Nonce()
480+
if err != nil {
481+
p.ErrorPage(rw, 500, "Internal Error", err.Error())
482+
return
483+
}
484+
p.SetCSRFCookie(rw, req, nonce)
461485
redirect, err := p.GetRedirect(req)
462486
if err != nil {
463487
p.ErrorPage(rw, 500, "Internal Error", err.Error())
464488
return
465489
}
466490
redirectURI := p.GetRedirectURI(req.Host)
467-
http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, redirect), 302)
491+
http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), 302)
468492
}
469493

470494
func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
@@ -489,8 +513,26 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
489513
return
490514
}
491515

492-
redirect := req.Form.Get("state")
493-
if !strings.HasPrefix(redirect, "/") || strings.HasPrefix(redirect, "//") {
516+
s := strings.SplitN(req.Form.Get("state"), ":", 2)
517+
if len(s) != 2 {
518+
p.ErrorPage(rw, 500, "Internal Error", "Invalid State")
519+
return
520+
}
521+
nonce := s[0]
522+
redirect := s[1]
523+
c, err := req.Cookie(p.CSRFCookieName)
524+
if err != nil {
525+
p.ErrorPage(rw, 403, "Permission Denied", err.Error())
526+
return
527+
}
528+
p.ClearCSRFCookie(rw, req)
529+
if c.Value != nonce {
530+
log.Printf("%s csrf token mismatch, potential attack", remoteAddr)
531+
p.ErrorPage(rw, 403, "Permission Denied", "csrf failed")
532+
return
533+
}
534+
535+
if !strings.HasPrefix(redirect, "/") || strings.HasPrefix(redirect, "//") {
494536
redirect = "/"
495537
}
496538

@@ -589,7 +631,7 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
589631
}
590632

591633
if clearSession {
592-
p.ClearCookie(rw, req)
634+
p.ClearSessionCookie(rw, req)
593635
}
594636

595637
if session == nil {

providers/provider_default.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"io/ioutil"
99
"net/http"
1010
"net/url"
11-
"strings"
1211

1312
"github.com/bitly/oauth2_proxy/cookie"
1413
)
@@ -79,7 +78,7 @@ func (p *ProviderData) Redeem(redirectURL, code string) (s *SessionState, err er
7978
}
8079

8180
// GetLoginURL with typical oauth parameters
82-
func (p *ProviderData) GetLoginURL(redirectURI, finalRedirect string) string {
81+
func (p *ProviderData) GetLoginURL(redirectURI, state string) string {
8382
var a url.URL
8483
a = *p.LoginURL
8584
params, _ := url.ParseQuery(a.RawQuery)
@@ -88,9 +87,7 @@ func (p *ProviderData) GetLoginURL(redirectURI, finalRedirect string) string {
8887
params.Add("scope", p.Scope)
8988
params.Set("client_id", p.ClientID)
9089
params.Set("response_type", "code")
91-
if strings.HasPrefix(finalRedirect, "/") && !strings.HasPrefix(finalRedirect,"//") {
92-
params.Add("state", finalRedirect)
93-
}
90+
params.Add("state", state)
9491
a.RawQuery = params.Encode()
9592
return a.String()
9693
}

0 commit comments

Comments
 (0)