Scala/Play framework

Play Framework 2.5. Built-in Filters, CSRF Filter, Custom Filter

partner_jun 2017. 4. 16. 17:43

Filter는 특정 URL에 영향을 미치는 Action과 달리 Gzip을 이용한 HTTP 압축이나 로깅, 보안 헤더 등 사이트 전체에 필요한 요소를 정의한다. 공식 문서에서는 모든 경로에 필요한 작업이 아니라면 ActionBuilder를 이용해 만든 Action을 사용하는 것이 더 좋다고 소개하고 있다.


0. Dependency 추가

filter를 사용하려면 build.sbt 파일의 libraryDependencies 항목에 filters를 추가해야 한다.


1. Built-in Http Filters

Play Framework는 4개의 필터를 기본으로 제공한다. 기본으로 제공되는 필터들은 DefaultHttpFilters 클래스를 상속받은 필터 클래스에 주입 받아 DefaultHttpFilters 클래스의 생성자로 전달해 사용 할 수 있다.


0) 준비

필터는 사이트에 하나만 적용된다. 그렇기 때문에 모든 필터는 기본 Http 필터에 추가되는 형식이다. 먼저 DefaultHttpFilters 클래스를 상속한 기본 필터를 정의한다.

class MyFilters @Inject()() extends DefaultHttpFilters()

filters 패키지의 MyFilter 클래스


그리고 conf/application.conf 파일에 필터 사용 구문을 추가한다.

play.http.filters = "filters.MyFilters"

filters 패키지 안의 MyFilter 클래스를 필터로 추가한다.



1) GzipFilter

미디어를 압축해 전송하는 Content-Coding 헤더를 추가한다. 압축 해제의 오버헤드 때문에 용량이 작다면 오히려 사용하지 않는 것이 더 빠를 수 있다.

class MyFilters @Inject()(gzipFilter: GzipFilter) extends DefaultHttpFilters(gzipFilter)

특정 Content-Type이나 Path에 대해서만 적용하려면 GzipFilter를 주입받지 않고 직접 객체를 만들어 추가해야 한다. 직접 GzipFilter 객체를 만들어 추가하는 방법은 Custom Filter에 작성한다.



2) SecurityHeadersFilter

class MyFilters @Inject()(shf: SecurityHeadersFilter) extends DefaultHttpFilters(shf)

SecurityHeadersFilter를 추가하면 5개의 헤더가 추가된다. 중요한 요소들이기 때문에 짚고 넘어가는 것이 좋을 것 같다. 추가되는 헤더들과 그 뜻은 다음과 같다.

 헤더명

 설정 가능 값(밑줄이 기본)

효과 

application.conf 설정

play.filters.headers. 이후 추가

(null 설정시 헤더 추가하지 않음)

 X-Frame-Options


 "DENY"


 "SAMEORIGIN" 


 사이트에 iframe 태그로 접근 할 수 없게 한다.

frameOptions

 X-XSS-Protection


 "0" : 필터 사용안함


 "1" : 필터 사용. 공격 감지되면 페이지 삭제


 "1;mode=block" : 필터 사용. 공격 감지되면 페이지 렌더링 차단


 "1; report=<reporting-uri>" : 필터 사용. 공격 감지되면 페이지 삭제하고 uri에 보고


 코드를 삽입한 입력을 방지한다.(input 박스에 <script> 등)

xssProtection

 X-Content-Type-Options

 "nosniff"

 다른 사이트에서 이 사이트의 MIME 타입을 스니핑하지 못하게 한다. 정해진 MIME 타입만을 따르게 된다. 

contentTypeOptions

 X-Permitted-Cross-Domain-Policies


 "master-only"


 "none"


 Flash나 PDF 파일에서 웹 사이트 외의 파일에 접근하지 못하게 한다.

permittedCrossDomainPolicies

 Content-Security-Policy


 "default-src 'self'"


 "unsafe-inline"


 자세한 설정은 모질라 제단을 확인


서버에서 전송하지 않은 script, css, 이미지 등의 요소를 허용하지 않는다.

contentSecurityPolicy


inline script / css에 영향을 주는 Content-Security-Policy는 숙지하고 정확히 설정하거나 포기하고 null을 설정해 추가하지 않는 것이 속 편하다.



3) CORSFilter

class MyFilters @Inject()(corsFilter: CORSFilter) extends DefaultHttpFilters(corsFilter)

XMLHttpRequest가 다른 사이트(Cross-site)에 요청이 가능하게한다. 추가되는 헤더와 헤더의 세부 설정은 공식 문서를 참조하자.



4) AllowedHostsFilter

class MyFilter @Inject()(ahf: AllowedHostsFilter) extends DefaultHttpFilters(ahf)


conf/application.conf 파일에 허용할 Host를 추가한다.

play.filters.hosts.allowed = ["localhost:9000"]

localhost:9000으로 전송된 Request만 허용한다. IP로 접속하면 Bad Request처리된다.




2. CSRF Filter

옥션의 개인정보 유출이 대표적인 CSRF 공격을 방지하기 위한 CSRF Filter도 Play Framework가 기본적으로 제공하는 필터 중 하나다. 필터를 추가하면 POST나 PUT Request시 발급받은 토큰을 전달해야 한다. 


1) 필터 추가

필터를 추가하는 방법은 다른 기본 필터와 같다.

class MyFilters @Inject()(csrfFilter: CSRFFilter) extends DefaultHttpFilters(csrfFilter)


필터를 추가하면 Response header와 Request session에 csrfToken이 추가된다.

크롬 확장 프로그램 HTTP Headers를 이용해 확인한 모습


RequestHeader를 암시적 인자로 받는 CSRF.getToken 메소드를 이용해 직접 생성할 수도 있다.

val token = CSRF.getToken.get

CSRF.getToken 메소드는 Option[Token]을 반환한다.


이렇게 만들어진 토큰은 POST나 PUT Request시에 csrfToken이라는 이름으로 전송해야한다. 발급된 토큰과 일치하지 않거나 전송하지 않는 경우 Unauthorized 처리된다.




2) CSRF Token 뷰에 추가

RequestHeader의 session에서 직접 참조하거나 폼을 만들어주는 helper를 이용해 뷰에 추가 할 수 있다.

def index = Action{ implicit req =>
Ok(views.html.testGET())
}

RequestHeader를 암시적 인자로 선언

@()(implicit req: RequestHeader)

<form method="POST" action="@routes.SimpleController.index2()">
<input type="hidden" name="csrfToken" value="@req.session("csrfToken")"/>
<button type="submit">submit</button>
</form>

암시적으로 전달된 RequestHeader의 session에서 토큰을 꺼내 "csrfToken"이라는 name으로 전달



def index = Action{ implicit req =>
Ok(views.html.testGET())
}
@import helper._
@()(implicit req: RequestHeader)

<form method="POST" action="@routes.SimpleController.index2()">
@CSRF.formField
<button type="submit">submit</button>
</form>

helper 패키지의 CSRF.formField 메소드를 사용해 전달


토큰의 버퍼 사이즈나 이름 변경등의 상세 설정은 공식 문서를 참조하자.



3. Custom Filter

필요에 따라 특정 기능을 수행하는 필터를 만들어 추가할 수 있다. 먼저 Filter를 상속받아 새로운 필터를 만들어 보자.

@Singleton
class CustomFilter @Inject()
(implicit val mat: Materializer,
implicit val ec: ExecutionContext) extends Filter {

override def apply(f: (RequestHeader) => Future[Result])
(rh: RequestHeader): Future[Result] = {

f(rh).map ( result => result.withHeaders("Hello" -> "World") )
// 다음 필터를 적용한 후 돌아온 결과에 이 필터의 결과를 추가한다.
}
}

이 필터가 실행되면 "Hello" 헤더에 "World" 값이 추가된다.


Filter를 상속받아 새로운 필터를 만들기 위해서는 Materializer 객체가 암시적으로 필요하다. 또, 다음 필터에 해당하는 함수 f의 결과에 map 함수를 적용하기 위해 ExecutionContext도 필요하다.


이렇게 만들어진 필터는 HttpFilters 트레잇을 상속받은 클래스의 filters 메소드에 추가해야 한다.

class MyCustomFilters @Inject()
(customFilter: CustomFilter)
(csrfFilter: CSRFFilter)
(implicit val mat: Materializer) extends HttpFilters {
// Gzip과 객체를 클래스에서 직접 만들기 때문에
// Materializer가 필요하다.

val customGzipFilter = new GzipFilter(shouldGzip = (request, response) =>
response.body.contentType.exists(_.startsWith("text/html")))
// text/html contentType에만 적용되는 GzipFilter

override def filters: Seq[EssentialFilter] = Seq( customGzipFilter, customFilter, csrfFilter )
}

주입받은 CSRFFilter를 보면 알 수 있듯, Play Framework가 기본으로 제공하는 필터도 추가가 가능하다.



만들어진 클래스를 conf/application.conf 파일에 적으면 필터들이 적용된다.

play.http.filters = "filters.MyCustomFilters"




"Hello" 헤더와 "World" 값이 추가된 모습