let's go를 배워보자 11일차 - User Authentication 2

0

lets-go

목록 보기
12/15

14. SameSite cookies

CSRF 공격을 막을 수 있는 방법 중 하나는 session cookie에 SameSite attribute를 설정해놓는 것이다.

golangcollege/sessions package는 항상 SameSite=Lax로 session cookie를 설정해놓는다. 이는 user의 브라우저에서 cross-site usage을 위해 session cookie을 전달하지 않는다는 것이다. 이를 통해서도 CSRF attack 위험을 줄일 수 있다.

별도로 만약 SameSite=Strict로 설정해놓고 싶다면, main.go 파일에 다음과 같이 설정이 가능하다.

  • main.go
...
session := sessions.New([]byte(*secret))
session.Lifetime = 12 * time.Hour
session.Secure = true
session.SameSite = http.SameSiteStrictMode
...

SameSite=Strict로 설정해놓으면 session cookie가 모든 cross-site usage에 대해서 user의 브라우저에서 전달되지 않는다. 이는 user가 우리의 사이트에서 외부의 link를 눌러도 전달되지 않는다는 것이다.

하지만 안타깝게도 모든 browser가 SameSite을 지원하지 않는다. 85%의 브라우저만이 이를 지원하므로 다른 방법이 필요하다.

15. Token-based Mitigation

이를 위해 token check form을 구현할 필요가 있다. 그러나 이를 직접 구현하기에는 여러 가지 방법들이 있고 다소 난이도가 있다. 따라서 누군가 구현해놓은 third-party를 사용하는 것이 좋다.

두 가지 유명한 패키지가 있는데 gorilla/csrfjustinas/nosurf가 있다. 이들은 Double Submit Cookie pattern을 사용하여 공격을 막는 것으로 상당 부분 동일하다. 이 패턴에서는 랜덤한 CSRF token을 만들어 CSRF cookie에 넣어 user에 전달해준다. CSRF token은 CSRF에 취약한 각 form의 hidden field에 추가된다. form이 submit되면 두 패키지는 hidden field value를 check하고 cookie 값이 매칭되는 지 확인한다.

우리는 justinas/nosurf 패키지를 사용할 것이다.

go get github.com/justinas/nosurf@v1

설치해보고 사용해보도록 하자. cmd/web/middleware.go 파일을 열고 noSurf() 함수를 만들어보도록 하자.

  • cmd/web/middleware.go
// Create a NoSurf middleware function which uses a customized CSRF cookie with
// the Secure, Path and HttpOnly flags set.
func noSurf(next http.Handler) http.Handler {
	csrfHandler := nosurf.New(next)
	csrfHandler.SetBaseCookie(http.Cookie{
		HttpOnly: true,
		Path:     "/",
		Secure:   true,
	})

	return csrfHandler
}

우리가 CSRF 공격으로부터 지키기 위한 form들 중 하나는 logout form이다. 이는 base.layout.tmpl 파일에 있으므로, 모든 페이지에서 노출될 가능이 있다. 때문에 noSurf middleware를 우리의 application 모든 route에 넣어주는 것이 좋다. 단, /static/은 할 필요가 없다.

그래서 cmd/web/routes.go 파일에 noSurf 미들웨어를 dynamicMiddleware에 추가하도록 하자.

package main

import (
	"net/http"

	"github.com/bmizerany/pat"
	"github.com/justinas/alice"
)

func (app *application) routes() http.Handler {
	standardMiddleware := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
	// Use the nosurf middleware on all our 'dynamic' routes.
	dynamicMiddleware := alice.New(app.session.Enable, noSurf)
	authenticatedMiddleware := dynamicMiddleware.Append(app.requireAuthentication)

	mux := pat.New()
	mux.Get("/", dynamicMiddleware.ThenFunc(app.home))
	mux.Get("/snippet/create", authenticatedMiddleware.ThenFunc(app.createSnippetForm))
	mux.Post("/snippet/create", authenticatedMiddleware.ThenFunc(app.createSnippet))
	mux.Get("/snippet/:id", dynamicMiddleware.ThenFunc(app.showSnippet))
	// Add the five new routes.
	mux.Get("/user/signup", dynamicMiddleware.ThenFunc(app.signupUserForm))
	mux.Post("/user/signup", dynamicMiddleware.ThenFunc(app.signupUser))
	mux.Get("/user/login", dynamicMiddleware.ThenFunc(app.loginUserForm))
	mux.Post("/user/login", dynamicMiddleware.ThenFunc(app.loginUser))
	mux.Post("/user/logout", authenticatedMiddleware.ThenFunc(app.logoutUser))

	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Get("/static/", http.StripPrefix("/static", fileServer))
	return standardMiddleware.Then(mux)
}

서버를 실행하여 잘 동작하는 지를 확인하고 싶겠지만, 아직 완성된 것은 아니다. 쿠키에 대한 설정은 끝났지만 CSRF token은 아직 설정되지 않았다.

즉, 각 form에 CSRF token을 hidden field에 넣어주어 매 요청마다 CSRF token을 확인하면 된다.

page에 전송되는 templateDataCSRFToken을 추가하여 전송하도록 하자.

  • cmd/web/templates.go
...
type templateData struct {
	CSRFToken       string
	CurrentYear     int
	Flash           string
	Form            *forms.Form
	IsAuthenticated bool
	Snippet         *models.Snippet
	Snippets        []*models.Snippet
}
...

logout form은 잠재적으로 모든 페이지에서 나올 수 있으므로, CSRF token을 모든 template data에 추가하는 것이 좋다. 그래서 addDefaultData() helper에 넣으면 된다.

  • cmd/web/helpers.go
...
func (app *application) addDefaultData(td *templateData, r *http.Request) *templateData {
	if td == nil {
		td = &templateData{}
	}
	td.CSRFToken = nosurf.Token(r)
	td.CurrentYear = time.Now().Year()
	td.Flash = app.session.PopString(r, "flash")
	td.IsAuthenticated = app.isAuthenticated(r)
	return td
}
...

이제 우리의 모든 form들을 수정하여 token을 사용할 수 있도록 하자.

  • ui/html/base.layout.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
        <link rel='stylesheet' href='/static/css/main.css'>
        <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
        <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <nav>
        <!-- Update the navigation to include signup, login and logout links -->
            <div>
                <a href='/'>Home</a>
                <!-- Toggle the navigation link -->
                {{if .IsAuthenticated}}
                    <a href='/snippet/create'>Create snippet</a>
                {{end}}
            </div>
            <div>
                <!-- Toggle the navigation links -->
                {{if .IsAuthenticated}}
                    <form action='/user/logout' method='POST'>
                        <!-- Include the CSRF token -->
                        <input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
                        <button>Logout</button>
                    </form>
                {{else}}
                    <a href='/user/signup'>Signup</a>
                    <a href='/user/login'>Login</a>
                {{end}}
            </div>
        </nav>
        <main>
            {{with .Flash}}
            <div class='flash'>{{.}}</div>
            {{end}}
            {{template "main" .}}
        </main>
        {{template "footer" .}}
        <script src="/static/js/main.js" type="text/javascript"></script>
    </body>
</html>
{{end}}
  • ui/html/create.page.tmpl
{{template "base" .}}

{{define "title"}}Create a New Snippet{{end}}

{{define "main"}}
<form action='/snippet/create' method='POST'>
    <!-- Include the CSRF token -->
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
    {{with .Form}}
        <div>
            <label>Title:</label>
            {{with .Errors.Get "title"}}
                <label class='error'>{{.}}</label>
            {{end}}
            <input type='text' name='title' value='{{.Get "title"}}'>
        </div>
        <div>
            <label>Content:</label>
            {{with .Errors.Get "content"}}
                <label class='error'>{{.}}</label>
            {{end}}
            <textarea name='content'>{{.Get "content"}}</textarea>
        </div>
        <div>
            <label>Delete in:</label>
            {{with .Errors.Get "expires"}}
                <label class='error'>{{.}}</label>
            {{end}}
            {{$exp := or (.Get "expires") "365"}}
            <input type='radio' name='expires' value='365' {{if (eq $exp "365")}}checked{{end}}> One Year
            <input type='radio' name='expires' value='7' {{if (eq $exp "7")}}checked{{end}}> One Week
            <input type='radio' name='expires' value='1' {{if (eq $exp "1")}}checked{{end}}> One Day
        </div>
        <div>
            <input type='submit' value='Publish snippet'>
        </div>
    {{end}}
</form>
{{end}}
  • ui/html/login.page.tmpl
{{template "base" .}}
{{define "title"}}Login{{end}}
{{define "main"}}
<form action='/user/login' method='POST' novalidate>
    <!-- Include the CSRF token -->
    <input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
    {{with .Form}}
        {{with .Errors.Get "generic"}}
            <div class='error'>{{.}}</div>
        {{end}}
        <div>
            <label>Email:</label>
            <input type='email' name='email' value='{{.Get "email"}}'>
        </div>
        <div>
            <label>Password:</label>
            <input type='password' name='password'>
        </div>
        <div>
            <input type='submit' value='Login'>
        </div>
    {{end}}
</form>
{{end}}
  • ui/html/signup.page.tmpl
{{template "base" .}}

{{define "title"}}Signup{{end}}

{{define "main"}}
<form action='/user/signup' method='POST' novalidate>
    <!-- Include the CSRF token -->
    <input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
    {{with .Form}}
        <div>
            <label>Name:</label>
            {{with .Errors.Get "name"}}
                <label class='error'>{{.}}</label>
            {{end}}
            <input type='text' name='name' value='{{.Get "name"}}'>
        </div>
        <div>
            <label>Email:</label>
            {{with .Errors.Get "email"}}
                <label class='error'>{{.}}</label>
            {{end}}
            <input type='email' name='email' value='{{.Get "email"}}'>
        </div>
        <div>
            <label>Password:</label>
            {{with .Errors.Get "password"}}
                <label class='error'>{{.}}</label>
            {{end}}
            <input type='password' name='password'>
        </div>
        <div>
            <input type='submit' value='Signup'>
        </div>
    {{end}}
</form>
{{end}}

이제 모든 설정이 끝났고, CSRF token이 숨겨져 있다. 잘되는 지 실행해보도록 하자.

개발자 디버깅으로 확인할 때 CSRF token이 있다면 성공한 것이다.

0개의 댓글