From 076fc98d981aea3533eea363ca1c7e43f77b9802 Mon Sep 17 00:00:00 2001 From: slene Date: Sun, 23 Mar 2014 01:44:02 +0800 Subject: [PATCH] add csrf check --- modules/base/tool.go | 10 ++- modules/middleware/auth.go | 58 +++++++++-------- modules/middleware/context.go | 107 +++++++++++++++++++++++++++++++- modules/middleware/render.go | 5 +- public/js/app.js | 33 ++++++++++ templates/admin/users/edit.tmpl | 1 + templates/admin/users/new.tmpl | 1 + templates/base/head.tmpl | 1 + templates/repo/create.tmpl | 1 + templates/repo/setting.tmpl | 1 + templates/user/active.tmpl | 3 +- templates/user/delete.tmpl | 1 + templates/user/password.tmpl | 4 +- templates/user/publickey.tmpl | 1 + templates/user/setting.tmpl | 1 + templates/user/signin.tmpl | 1 + templates/user/signup.tmpl | 1 + web.go | 24 +++---- 18 files changed, 208 insertions(+), 46 deletions(-) diff --git a/modules/base/tool.go b/modules/base/tool.go index 8fabb8c531..a2aeebf1b8 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -25,13 +25,17 @@ func EncodeMd5(str string) string { return hex.EncodeToString(m.Sum(nil)) } -// Random generate string -func GetRandomString(n int) string { +// GetRandomString generate random string by specify chars. +func GetRandomString(n int, alphabets ...byte) string { const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" var bytes = make([]byte, n) rand.Read(bytes) for i, b := range bytes { - bytes[i] = alphanum[b%byte(len(alphanum))] + if len(alphabets) == 0 { + bytes[i] = alphanum[b%byte(len(alphanum))] + } else { + bytes[i] = alphabets[b%byte(len(alphabets))] + } } return string(bytes) } diff --git a/modules/middleware/auth.go b/modules/middleware/auth.go index f211de32b9..b557188ee9 100644 --- a/modules/middleware/auth.go +++ b/modules/middleware/auth.go @@ -10,39 +10,45 @@ import ( "github.com/gogits/gogs/modules/base" ) -// SignInRequire requires user to sign in. -func SignInRequire(redirect bool) martini.Handler { - return func(ctx *Context) { - if !ctx.IsSigned { - if redirect { - ctx.Redirect("/user/login") - } - return - } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { - ctx.Data["Title"] = "Activate Your Account" - ctx.HTML(200, "user/active") - return - } - } +type ToggleOptions struct { + SignInRequire bool + SignOutRequire bool + AdminRequire bool + DisableCsrf bool } -// SignOutRequire requires user to sign out. -func SignOutRequire() martini.Handler { +func Toggle(options *ToggleOptions) martini.Handler { return func(ctx *Context) { - if ctx.IsSigned { + if options.SignOutRequire && ctx.IsSigned { ctx.Redirect("/") return } - } -} -// AdminRequire requires user signed in as administor. -func AdminRequire() martini.Handler { - return func(ctx *Context) { - if !ctx.User.IsAdmin { - ctx.Error(403) - return + if !options.DisableCsrf { + if ctx.Req.Method == "POST" { + if !ctx.CsrfTokenValid() { + ctx.Error(403, "CSRF token does not match") + return + } + } + } + + if options.SignInRequire { + if !ctx.IsSigned { + ctx.Redirect("/user/login") + return + } else if !ctx.User.IsActive && base.Service.RegisterEmailConfirm { + ctx.Data["Title"] = "Activate Your Account" + ctx.HTML(200, "user/active") + return + } + } + + if options.AdminRequire { + if !ctx.User.IsAdmin { + ctx.Error(403) + return + } } - ctx.Data["PageIsAdmin"] = true } } diff --git a/modules/middleware/context.go b/modules/middleware/context.go index c958c1d6cd..b28953fc0e 100644 --- a/modules/middleware/context.go +++ b/modules/middleware/context.go @@ -6,6 +6,7 @@ package middleware import ( "fmt" + "html/template" "net/http" "time" @@ -32,6 +33,8 @@ type Context struct { User *models.User IsSigned bool + csrfToken string + Repo struct { IsValid bool IsOwner bool @@ -90,6 +93,95 @@ func (ctx *Context) Handle(status int, title string, err error) { ctx.HTML(status, fmt.Sprintf("status/%d", status)) } +func (ctx *Context) GetCookie(name string) string { + cookie, err := ctx.Req.Cookie(name) + if err != nil { + return "" + } + return cookie.Value +} + +func (ctx *Context) SetCookie(name string, value string, others ...interface{}) { + cookie := http.Cookie{} + cookie.Name = name + cookie.Value = value + + if len(others) > 0 { + switch v := others[0].(type) { + case int: + cookie.MaxAge = v + case int64: + cookie.MaxAge = int(v) + case int32: + cookie.MaxAge = int(v) + } + } + + // default "/" + if len(others) > 1 { + if v, ok := others[1].(string); ok && len(v) > 0 { + cookie.Path = v + } + } else { + cookie.Path = "/" + } + + // default empty + if len(others) > 2 { + if v, ok := others[2].(string); ok && len(v) > 0 { + cookie.Domain = v + } + } + + // default empty + if len(others) > 3 { + switch v := others[3].(type) { + case bool: + cookie.Secure = v + default: + if others[3] != nil { + cookie.Secure = true + } + } + } + + // default false. for session cookie default true + if len(others) > 4 { + if v, ok := others[4].(bool); ok && v { + cookie.HttpOnly = true + } + } + + ctx.Res.Header().Add("Set-Cookie", cookie.String()) +} + +func (ctx *Context) CsrfToken() string { + if len(ctx.csrfToken) > 0 { + return ctx.csrfToken + } + + token := ctx.GetCookie("_csrf") + if len(token) == 0 { + token = base.GetRandomString(30) + ctx.SetCookie("_csrf", token) + } + ctx.csrfToken = token + return token +} + +func (ctx *Context) CsrfTokenValid() bool { + token := ctx.Query("_csrf") + if token == "" { + token = ctx.Req.Header.Get("X-Csrf-Token") + } + if token == "" { + return false + } else if ctx.csrfToken != token { + return false + } + return true +} + // InitContext initializes a classic context for a request. func InitContext() martini.Handler { return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { @@ -103,11 +195,14 @@ func InitContext() martini.Handler { Render: rd, } + ctx.Data["PageStartTime"] = time.Now() + // start session ctx.Session = base.SessionManager.SessionStart(res, r) - defer func() { + rw := res.(martini.ResponseWriter) + rw.Before(func(martini.ResponseWriter) { ctx.Session.SessionRelease(res) - }() + }) // Get user from session if logined. user := auth.SignedInUser(ctx.Session) @@ -121,9 +216,15 @@ func InitContext() martini.Handler { ctx.Data["SignedUserId"] = user.Id ctx.Data["SignedUserName"] = user.LowerName ctx.Data["IsAdmin"] = ctx.User.IsAdmin + + if ctx.User.IsAdmin { + ctx.Data["PageIsAdmin"] = true + } } - ctx.Data["PageStartTime"] = time.Now() + // get or create csrf token + ctx.Data["CsrfToken"] = ctx.CsrfToken() + ctx.Data["CsrfTokenHtml"] = template.HTML(``) c.Map(ctx) diff --git a/modules/middleware/render.go b/modules/middleware/render.go index 8a54183135..869ef9abaa 100644 --- a/modules/middleware/render.go +++ b/modules/middleware/render.go @@ -242,8 +242,11 @@ func (r *Render) HTMLString(name string, binding interface{}, htmlOpt ...HTMLOpt } } -func (r *Render) Error(status int) { +func (r *Render) Error(status int, message ...string) { r.WriteHeader(status) + if len(message) > 0 { + r.Write([]byte(message[0])) + } } func (r *Render) Redirect(location string, status ...int) { diff --git a/public/js/app.js b/public/js/app.js index f179342f4b..df755727b5 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -2,6 +2,39 @@ var Gogits = { "PageIsSignup": false }; +(function($){ + // extend jQuery ajax, set csrf token value + var ajax = $.ajax; + $.extend({ + ajax: function(url, options) { + if (typeof url === 'object') { + options = url; + url = undefined; + } + options = options || {}; + url = options.url; + var csrftoken = $('meta[name=_csrf]').attr('content'); + var headers = options.headers || {}; + var domain = document.domain.replace(/\./ig, '\\.'); + if (!/^(http:|https:).*/.test(url) || eval('/^(http:|https:)\\/\\/(.+\\.)*' + domain + '.*/').test(url)) { + headers = $.extend(headers, {'X-Csrf-Token':csrftoken}); + } + options.headers = headers; + var callback = options.success; + options.success = function(data){ + if(data.once){ + // change all _once value if ajax data.once exist + $('[name=_once]').val(data.once); + } + if(callback){ + callback.apply(this, arguments); + } + }; + return ajax(url, options); + } + }); +}(jQuery)); + (function ($) { Gogits.showTab = function (selector, index) { diff --git a/templates/admin/users/edit.tmpl b/templates/admin/users/edit.tmpl index 2a9882423a..08f11fcb12 100644 --- a/templates/admin/users/edit.tmpl +++ b/templates/admin/users/edit.tmpl @@ -12,6 +12,7 @@
{{if .IsSuccess}}

Account profile has been successfully updated.

{{else if .HasError}}

{{.ErrorMsg}}

{{end}} + {{.CsrfTokenHtml}}
diff --git a/templates/admin/users/new.tmpl b/templates/admin/users/new.tmpl index 01d976caa0..7b41ae43a7 100644 --- a/templates/admin/users/new.tmpl +++ b/templates/admin/users/new.tmpl @@ -11,6 +11,7 @@

+ {{.CsrfTokenHtml}}
{{.ErrorMsg}}
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index f02ea095ca..7f56ed7080 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -8,6 +8,7 @@ + diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 2de92f515f..a43f510484 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -2,6 +2,7 @@ {{template "base/navbar" .}}
+ {{.CsrfTokenHtml}}

Create New Repository

{{.ErrorMsg}}
diff --git a/templates/repo/setting.tmpl b/templates/repo/setting.tmpl index a2fb1771d4..38c3fd3bcc 100644 --- a/templates/repo/setting.tmpl +++ b/templates/repo/setting.tmpl @@ -40,6 +40,7 @@