]> source.dussan.org Git - gitea.git/commitdiff
add csrf check
authorslene <vslene@gmail.com>
Sat, 22 Mar 2014 17:44:02 +0000 (01:44 +0800)
committerslene <vslene@gmail.com>
Sat, 22 Mar 2014 17:44:02 +0000 (01:44 +0800)
18 files changed:
modules/base/tool.go
modules/middleware/auth.go
modules/middleware/context.go
modules/middleware/render.go
public/js/app.js
templates/admin/users/edit.tmpl
templates/admin/users/new.tmpl
templates/base/head.tmpl
templates/repo/create.tmpl
templates/repo/setting.tmpl
templates/user/active.tmpl
templates/user/delete.tmpl
templates/user/password.tmpl
templates/user/publickey.tmpl
templates/user/setting.tmpl
templates/user/signin.tmpl
templates/user/signup.tmpl
web.go

index 8fabb8c531d9ce042046ad5c074e93d637f3e995..a2aeebf1b81ad2c16a3732047384d288b0e0c1fa 100644 (file)
@@ -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)
 }
index f211de32b9030c0195b9c7dc7731d08ebf07813f..b557188ee92bd8298d4057edaa26dbd950301e2c 100644 (file)
@@ -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
        }
 }
index c958c1d6cd8bdcbbda6d62c4b32261cca32fa925..b28953fc0ede9c7f18a19b070ef1531ad956a529 100644 (file)
@@ -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(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`)
 
                c.Map(ctx)
 
index 8a541831350d366d25dddb14042c89655a09d311..869ef9abaa834eefe77ab6e594ae1ff03574df36 100644 (file)
@@ -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) {
index f179342f4b4c68ae5b504d830fac4e3c5f6e420b..df755727b508f9ae726bba461df923d416f4130f 100644 (file)
@@ -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) {
index 2a9882423a8f2839602c0156f30ccd22ebbb0f09..08f11fcb128d739338f7fd6ee07e60392a63fa3e 100644 (file)
@@ -12,6 +12,7 @@
                <br/>
                                <form action="/admin/users/{{.User.Id}}" method="post" class="form-horizontal">
                                    {{if .IsSuccess}}<p class="alert alert-success">Account profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
+                                   {{.CsrfTokenHtml}}
                        <input type="hidden" value="{{.User.Id}}" name="userId"/>
                                        <div class="form-group">
                                                <label class="col-md-3 control-label">Username: </label>
index 01d976caa0ba1c3cccb8c3284b46c9a1b6cd4b84..7b41ae43a7ad6373607d7f87c031b08b100bc8d6 100644 (file)
@@ -11,6 +11,7 @@
             <div class="panel-body">
                <br/>
                                <form action="/admin/users/new" method="post" class="form-horizontal">
+                                       {{.CsrfTokenHtml}}
                                    <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
                                        <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
                                                <label class="col-md-3 control-label">Username: </label>
index f02ea095cafd1498089f54b79c374e0dc0b33f38..7f56ed7080664690c8cbac6f9c98a6b904b33144 100644 (file)
@@ -8,6 +8,7 @@
         <meta name="author" content="Gogs - Go Git Service" />
                <meta name="description" content="Gogs(Go Git Service) is a GitHub-like clone in the Go Programming Language" />
                <meta name="keywords" content="go, git">
+               <meta name="_csrf" content="{{.CsrfToken}}" />
 
                 <!-- Stylesheets -->
                <link href="/css/bootstrap.min.css" rel="stylesheet" />
index 2de92f515fddcf6c4d7ba777ded8d9124cac3125..a43f5104845d18e9c7c0711a1a74349dbfae9d6d 100644 (file)
@@ -2,6 +2,7 @@
 {{template "base/navbar" .}}
 <div class="container" id="gogs-body">
     <form action="/repo/create" method="post" class="form-horizontal gogs-card" id="gogs-repo-create">
+        {{.CsrfTokenHtml}}
         <h3>Create New Repository</h3>
         <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
         <div class="form-group">
index a2fb1771d4416a2bbbe9c5032165d7d55f49e5d5..38c3fd3bcc7db3f7ca499e6c1a44a2c99c61ec12 100644 (file)
@@ -40,6 +40,7 @@
                 <div class="modal fade" id="delete-repository-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
                     <div class="modal-dialog">
                         <form action="/{{.Owner.Name}}/{{.Repository.Name}}/settings" method="post" class="modal-content">
+                            {{.CsrfTokenHtml}}
                             <input type="hidden" name="action" value="delete">
 
                             <div class="modal-header">
index 47c87a591c6c0e778ce9b4ce1359711f276b289e..0df116cb4052d8d3070cebb929a7e569e8c60f19 100644 (file)
@@ -1,7 +1,8 @@
 {{template "base/head" .}}
 {{template "base/navbar" .}}
 <div id="gogs-body" class="container">
-    <form action="/user/activate" method="get" class="form-horizontal gogs-card" id="gogs-login-card">
+    <form action="/user/activate" method="post" class="form-horizontal gogs-card" id="gogs-login-card">
+        {{.CsrfTokenHtml}}
         <h3>Activate Your Account</h3>
         {{if .IsActivatePage}}
             {{if .ServiceNotEnabled}}
index 397ea8cc09c00d9326e3f5f9429dfbd3248fc8b8..46376672d4e980ed849dc92435723078888848a2 100644 (file)
@@ -22,6 +22,7 @@
     <div class="modal fade" id="delete-account-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
         <div class="modal-dialog">
             <form action="/user/delete" method="post" class="modal-content" id="gogs-user-delete">
+                {{.CsrfTokenHtml}}
                 <div class="modal-header">
                     <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                     <h4 class="modal-title" id="myModalLabel">Delete Account</h4>
index 2ee178a3fc7c6e498e4abe51775feb3bc80a448d..936ec4b1242b4f11c605d131a25ef878d752f25b 100644 (file)
@@ -5,7 +5,9 @@
     <div id="gogs-user-setting-container" class="col-md-9">
         <div id="gogs-setting-pwd">
             <h4>Password</h4>
-            <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting/password">{{if .IsSuccess}}
+            <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting/password">
+            {{.CsrfTokenHtml}}
+            {{if .IsSuccess}}
                 <p class="alert alert-success">Password is changed successfully. You can now sign in via new password.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
                 <div class="form-group">
                     <label class="col-md-3 control-label">Old Password<strong class="text-danger">*</strong></label>
index 72467659bef06eebdc4811639fecb3b788f271d2..e645e1a84b0858b9a2c846ceb5cff666e03d10c2 100644 (file)
@@ -22,6 +22,7 @@
             <div class="modal fade" id="ssh-add-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
                 <div class="modal-dialog">
                     <form class="modal-content form-horizontal" id="gogs-ssh-form" method="post" action="/user/setting/ssh/">
+                        {{.CsrfTokenHtml}}
                         <div class="modal-header">
                             <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                             <h4 class="modal-title" id="myModalLabel">Add SSH Key</h4>
index 222ddd895b9b1c41cd0030e0ccf8b1815afe1434..30c9529b1263f915762a5c2602451c1e7fc035b3 100644 (file)
@@ -6,6 +6,7 @@
         <div id="gogs-setting-pwd">
             <h4>Account Profile</h4>
             <form class="form-horizontal" id="gogs-password-form" method="post" action="/user/setting">
+                {{.CsrfTokenHtml}}
                 {{if .IsSuccess}}<p class="alert alert-success">Your profile has been successfully updated.</p>{{else if .HasError}}<p class="alert alert-danger form-error">{{.ErrorMsg}}</p>{{end}}
                 <p>Your Email will be public and used for Account related notifications and any web based operations made via the web.</p>
                 <div class="form-group">
index a49bf11405db3320b9958d14d83e3f85635e3ad4..8dc7292ff8b582a2b1b340ddeb3b81f6a1d1edbd 100644 (file)
@@ -2,6 +2,7 @@
 {{template "base/navbar" .}}
 <div class="container" id="gogs-body" data-page="user-signin">
     <form action="/user/login" method="post" class="form-horizontal gogs-card" id="gogs-login-card">
+        {{.CsrfTokenHtml}}
         <h3>Log in</h3>
         <div class="alert alert-danger form-error{{if .HasError}}{{else}} hidden{{end}}">{{.ErrorMsg}}</div>
         <div class="form-group {{if .Err_UserName}}has-error has-feedback{{end}}">
index 069d34a5b2c0a5bc9182401c4f73258b93517678..fbfc4cadccf71fc479af2029f3de6e0420a1704c 100644 (file)
@@ -2,6 +2,7 @@
 {{template "base/navbar" .}}
 <div class="container" id="gogs-body" data-page="user-signup">
        <form action="/user/sign_up" method="post" class="form-horizontal gogs-card" id="gogs-login-card">
+               {{.CsrfTokenHtml}}
                {{if .DisenableRegisteration}}
                Sorry, registeration has been disenabled, you can only get account from administrator.
                {{else}}
diff --git a/web.go b/web.go
index ac5761d720eed9ab182c5a596e2647caa79d30ff..0da2d129d091cacf60226f3421f98d2a26854ab1 100644 (file)
--- a/web.go
+++ b/web.go
@@ -82,9 +82,10 @@ func runWeb(*cli.Context) {
 
        m.Use(middleware.InitContext())
 
-       reqSignIn := middleware.SignInRequire(true)
-       ignSignIn := middleware.SignInRequire(base.Service.RequireSignInView)
-       reqSignOut := middleware.SignOutRequire()
+       reqSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true})
+       ignSignIn := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: base.Service.RequireSignInView})
+       reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true})
+
        // Routers.
        m.Get("/", ignSignIn, routers.Home)
        m.Get("/issues", reqSignIn, user.Issues)
@@ -109,14 +110,15 @@ func runWeb(*cli.Context) {
 
        m.Get("/help", routers.Help)
 
-       adminReq := middleware.AdminRequire()
-       m.Get("/admin", reqSignIn, adminReq, admin.Dashboard)
-       m.Get("/admin/users", reqSignIn, adminReq, admin.Users)
-       m.Any("/admin/users/new", reqSignIn, adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser)
-       m.Any("/admin/users/:userid", reqSignIn, adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser)
-       m.Any("/admin/users/:userid/delete", reqSignIn, adminReq, admin.DeleteUser)
-       m.Get("/admin/repos", reqSignIn, adminReq, admin.Repositories)
-       m.Get("/admin/config", reqSignIn, adminReq, admin.Config)
+       adminReq := middleware.Toggle(&middleware.ToggleOptions{SignInRequire: true, AdminRequire: true})
+
+       m.Get("/admin", adminReq, admin.Dashboard)
+       m.Get("/admin/users", adminReq, admin.Users)
+       m.Any("/admin/users/new", adminReq, binding.BindIgnErr(auth.RegisterForm{}), admin.NewUser)
+       m.Any("/admin/users/:userid", adminReq, binding.BindIgnErr(auth.AdminEditUserForm{}), admin.EditUser)
+       m.Any("/admin/users/:userid/delete", adminReq, admin.DeleteUser)
+       m.Get("/admin/repos", adminReq, admin.Repositories)
+       m.Get("/admin/config", adminReq, admin.Config)
 
        m.Post("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.SettingPost)
        m.Get("/:username/:reponame/settings", reqSignIn, middleware.RepoAssignment(true), repo.Setting)