6.7. CSRF対策¶
目次
6.7.1. Overview¶
- 秘密情報(トークン)の埋め込み
- パスワードの再入力
- Referのチェック
Note
OWASPとは
Open Web Application Security Projectの略称であり、信頼できるアプリケーションや、セキュリティに関する 効果的なアプローチなどを検証、提唱する、国際的な非営利団体である。
Picture - csrf check other kind
Tip
CSRFトークンチェックは、別サイトからの不正な更新リクエストをチェックし、エラーとするものである。 ユーザに順序性(一連の業務フロー)を守らせ、チェックするためには、トランザクショントークンチェックについてを参照されたい。
Warning
CSRF対策機能は、Spring Security3.2から提供される機能であるが、共通ライブラリ(terasoluna-gfw-security-web)の1.0.0.RELEASE版が依存している Spring Securityのバージョンは、3.1.4.RELEASEである(共通ライブラリの1.0.0.RELEASE版リリース時には、Spring Securityの3.2.0.RELEASE版は未リリースであるため)。 このため、terasoluna-gfw-security-webプロジェクト内に、1.0.0.RELEASE版リリース時のSprinng SecurityのCSRF対策機能に関する以下のクラスが同梱されている。
- org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy
- org.springframework.security.web.csrf.CsrfAuthenticationStrategy
- org.springframework.security.web.csrf.CsrfFilter
- org.springframework.security.web.csrf.CsrfLogoutHandler
- org.springframework.security.web.csrf.CsrfToken
- org.springframework.security.web.csrf.CsrfTokenRepository
- org.springframework.security.web.csrf.DefaultCsrfToken
- org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository
- org.springframework.security.web.csrf.InvalidCsrfTokenException
- org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor
共通ライブラリのバージョンアップのタイミングで、Spring Securityのバージョンアップし、上記のクラスは、terasoluna-gfw-security-webプロジェクトからは取り除かれる予定である。
6.7.2. How to use¶
6.7.2.1. Spring Securityの設定¶
Spring SecurityのCSRF機能を使用するための設定を説明する。 Spring Security の How to useで設定したweb.xmlを前提とする。
6.7.2.1.1. spring-security.xmlの設定¶
追加で設定が必要な箇所を、ハイライトしている。
<sec:http auto-config="true" use-expressions="true" >
<!-- omitted -->
<sec:custom-filter ref="csrfFilter" before="LOGOUT_FILTER" /> <!-- (1) -->
<sec:session-management
session-authentication-strategy-ref="sessionAuthenticationStrategy" /> <!-- (2) -->
<!-- omitted -->
</sec:http>
<bean id="csrfFilter" class="org.springframework.security.web.csrf.CsrfFilter"> <!-- (3) -->
<constructor-arg index="0" ref="csrfTokenRepository" /> <!-- (4) -->
<property name="accessDeniedHandler">
<bean
class="org.springframework.security.web.access.AccessDeniedHandlerImpl"> <!-- (5) -->
<property name="errorPage"
value="/WEB-INF/views/common/error/csrfTokenError.jsp" /> <!-- (6) -->
</bean>
</property>
</bean>
<bean id="csrfTokenRepository"
class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository" /> <!-- (7) -->
<bean id="sessionAuthenticationStrategy"
class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy"> <!-- (8) -->
<constructor-arg index="0">
<list>
<!-- omitted -->
<bean
class="org.springframework.security.web.csrf.CsrfAuthenticationStrategy"> <!-- (9) -->
<constructor-arg index="0"
ref="csrfTokenRepository" /> <!-- (10) -->
</bean>
</list>
</constructor-arg>
</bean>
項番 | 説明 |
---|---|
(1)
|
<sec:custom-filter> 要素を定義し、org.springframework.security.web.authentication.logout.LogoutFilter の前に CSRFのFilter定義を行う。 |
(2)
|
<sec:session-management> 要素の、session-authentication-strategy-ref 属性で、org.springframework.security.web.authentication.session.SessionAuthenticationStrategy を参照する。 |
(3)
|
org.springframework.security.web.csrf.CsrfFilter のbean定義を行う。 |
(4)
|
コンストラクタの第1引数で、トークンの作成、保持を行う
org.springframework.security.web.csrf.CsrfTokenRepository を参照する。 |
(5)
|
accessDeniedHandler プロパティにorg.springframework.security.web.access.AccessDeniedHandlerImpl を bean定義する。 |
(6)
|
AccessDeniedHandlerImpl のerrorPage プロパティに、リクエストに含まれるCSRFトークンが、一致しない場合の遷移先パスを設定する。設定を省略した場合、リクエストに含まれるCSRFトークンが一致しない場合、ステータスコード403でクライアントに返却する。
|
(7)
|
CsrfTokenRepository の実装としてHTTPセッションにCSRFトークンを保存する、org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository クラスを定義する。 |
(8)
|
SessionAuthenticationStrategy の実装として、複数のSessionAuthenticationStrategy を使用できるorg.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy |
(9)
|
CompositeSessionAuthenticationStrategy に、org.springframework.security.web.csrf.CsrfAuthenticationStrategy を追加する。 |
(10)
|
CompositeSessionAuthenticationStrategy コンストラクタの第1引数で、CsrfTokenRepository を参照する。 |
Note
AccessDeniedHandlerImplのerrorPageプロパティを省略した場合の、エラーハンドリングについて
web.xmlに、以下の設定を行うことで、任意のページに遷移させることができる。
web.xml
<error-page> <error-code>403</error-code> <!-- (1) --> <location>/WEB-INF/views/common/error/csrf-error.jsp</location> <!-- (2) --> </error-page>
項番 説明 (1) error-code要素に、ステータスコード403を設定する。 (2) location要素に、遷移先のパスを設定する。
Note
ステータスコード403以外を返却したい場合
リクエストに含まれるCSRFトークンが一致しない場合、ステータスコード403以外を返却したい場合は、org.springframework.security.web.access.AccessDeniedHandler
インタフェースを
実装した、独自のAccessDeniedHandlerを作成する必要がある。
詳細は、Spring Securityのレファレンスドキュメントを参照されたい。
Todo
Spring Security のバージョンが、 3.2.0 以降の場合の設定
Spring Security 3.2を使用する場合、<sec:http>
要素に<sec:csrf />
要素を設定することで、
前述した設定を省略することができる。
Spring Securityのレファレンスドキュメントを参照されたい。
6.7.2.1.2. spring-mvc.xmlの設定¶
CSRFトークン用のRequestDataValueProcessor
実装クラスを利用し、Springのタグライブラリの<form:form>
タグを使うことで、自動的にCSRFトークンを、hiddenに埋め込むことができる。
<bean id="requestDataValueProcessor"
class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor"> <!-- (1) -->
<constructor-arg>
<util:list>
<bean
class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor"
factory-method="create" /> <!-- (2) -->
<bean
class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
</util:list>
</constructor-arg>
</bean>
項番 | 説明 |
---|---|
(1)
|
org.terasoluna.gfw.web.mvc.support.RequestDataValueProcessor を複数定義可能な、org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor をbean定義する。 |
(2)
|
コンストラクタの第1引数に、
org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor のbean定義を設定する。factory-methodに、createメソッドを指定する。
|
Note
CSRFトークンの生成、チェックは、CsrfFilter
が行うため、Controllerでは特に、CSRF対策は意識しなくてよい。
6.7.2.2. フォームによるCSRFトークンの送信¶
JSPで、フォームからCSRFトークンを送信するには
<form:form>
タグを使用してCSRFトークンが埋め込まれた<input type="hidden">
タグを自動的に追加する<input type="hidden">
タグを作成し、明示的にCSRFトークンを埋め込む
のどちらかを行う必要がある。
6.7.2.2.1. CSRFトークンを自動で埋め込む方法¶
spring-mvc.xmlの設定の通り、CsrfRequestDataValueProcessor
が定義されている場合、
<form:form>
タグを使うことで、CSRFトークンが埋め込まれた<input type="hidden">
タグが、自動的に追加される。
JSPで、CSRFトークンを意識する必要はない。
<form:form method="POST"
action="${pageContext.request.contextPath}/csrfTokenCheckExample">
<input type="submit" name="second" value="second" />
</form:form>
以下のようなHTMLが、出力される。
<form action="/terasoluna/csrfTokenCheckExample" method="POST">
<input type="submit" name="second" value="second" />
<input type="hidden" name="_csrf" value="dea86ae8-58ea-4310-bde1-59805352dec7" /> <!-- (1) -->
</form>
項番 | 説明 |
---|---|
(1)
|
Spring Securityのデフォルト実装では、
name 属性に_csrf が設定されている <input type="hidden"> タグが追加され、CSRFトークンが埋め込まれる。 |
CSRFトークンはログインのタイミングで生成される。
Warning
CsrfRequestDataValueProcessor
を設定している状態で <form:form method="GET" ...>...</form:form>
とした場合(GETメソッドを指定してフォームを送信した場合)、
- ブラウザのアドレスバーにCSRFトークンが表示される
- ブックマークした場合、ブックマークにCSRFトークンが記録される
- WebサーバのアクセスログにCSRFトークンが記録される
ため、攻撃者にCSRFトークンを悪用されるリスクが高くなる。
この事象を回避するためには、
<form:form method="GET" modelAttribute="xxxForm" action="..."> ... </form:form>
と書く代わりに、
<form method="GET" action="..."> <spring:nestedPath path="xxxForm"> ... </spring:nestedPath> </form>`
と記述すればよい。
The unique token can also be included in the URL itself, or a URL parameter. However, such placement runs a greater risk that the URL will be exposed to an attacker, thus compromising the secret token.
と説明されており、必須ではないが対応することが推奨される。
Spring Securityのデフォルト実装では、CSRFトークンの値としてランダムなUUIDを生成しているため、 仮にCSRFトークンが漏洩してもセッションハイジャックされる事はないという点を補足しておく。
また、Spring 4を使用すると、この問題は解消される。(<form:form method="GET">
を使用してもCSRFトークンはURLに現れない)。
6.7.2.2.2. CSRFトークンを明示的に埋め込む方法¶
<form:form>
タグを使用しない場合は、明示的に、<input type="hidden">
タグを追加する必要がある。
CsrfFilter
により、org.springframework.security.web.csrf.CsrfToken
オブジェクトが、リクエストスコープの
_csrf
属性に設定されるため、jspでは、以下のように設定すればよい
<form method="POST"
action="${pageContext.request.contextPath}/csrfTokenCheckExample">
<input type="submit" name="second" value="second" />
<input type="hidden" name="${f:h(_csrf.parameterName)}" value="${f:h(_csrf.token)}"/> <!-- (1) -->
</form>
以下のようなHTMLが、出力される。
<form action="/terasoluna/csrfTokenCheckExample" method="POST">
<input type="submit" name="second" value="second" />
<input type="hidden" name="_csrf" value="dea86ae8-58ea-4310-bde1-59805352dec7"/> <!-- (2) -->
</form>
項番 | 説明 |
---|---|
(1)
|
_csrf.parameterName でリクエストパラメータ名を、_csrf.token で、CSRFトークンを設定する。 |
(2)
|
Spring Securityのデフォルト実装では、
name 属性に_csrf が設定されている <input type="hidden"> タグが追加され、CSRFトークンが埋め込まれる。 |
Note
CSRFトークンチェック対象のリクエスト(デフォルトでは、HTTPメソッドが、GET, HEAD, TRACE, OPTIONS以外の場合)で、CSRFトークンがない、または
サーバー上に保存されているトークン値と、送信されたトークン値が異なる場合は、AccessDeniedHandler
によりアクセス拒否処理が行われる。
デフォルトでは403エラーとなり、AccessDeniedHandlerImpl
のerrorPage
プロパティで指定したエラーページに遷移する。
詳細は、spring-security.xmlの設定を参照されたい。
6.7.2.3. AjaxによるCSRFトークンの送信¶
CsrfFilter
は、前述のようにリクエストパラメータからCSRFトークンを取得するだけでなく、Note
HTTPヘッダ、リクエストパラメータの両方からCSRFトークンが送信する場合は、HTTPヘッダの値が優先される。
jspの実装例
<!-- omitted -->
<head>
<meta name="_csrf" content="${f:h(_csrf.token)}"/> <!-- (1) -->
<meta name="_csrf_header" content="${f:h(_csrf.headerName)}"/> <!-- (2) -->
<!-- omitted -->
</head>
<!-- omitted -->
<script type="text/javascript">
var contextPath = "${pageContext.request.contextPath}";
var token = $("meta[name='_csrf']").attr("content"); <!-- (3) -->
var header = $("meta[name='_csrf_header']").attr("content"); <!-- (4) -->
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token); <!-- (5) -->
});
$(function() {
$('#calcButton').on('click', function() {
var $form = $('#calcForm'),
$result = $('#result');
$.ajax({
url : contextPath + '/sample/calc',
type : 'POST',
data: $form.serialize(),
}).done(function(data) {
$result.html('add: ' + data.addResult + '<br>'
+ 'subtract: ' + data.subtractResult + '<br>'
+ 'multipy: ' + data.multipyResult + '<br>'
+ 'divide: ' + data.divideResult + '<br>'); // (6)
}).fail(function(data) {
// error handling
alert(data.statusText);
});
});
});
</script>
項番 | 説明 |
---|---|
(1)
|
<meta> タグに、${f:h(_csrf.token)} で取得したCSRFトークンを設定する。 |
(2)
|
<meta> タグに、${f:h(_csrf.headerName)} で取得したヘッダ名を設定する。 |
(3)
|
<meta> タグに、設定したCSRFトークンを取得する。 |
(4)
|
<meta> タグに、設定したCSRFヘッダ名を取得する。 |
(5)
|
リクエストヘッダーに、
<meta> タグで設定したヘッダ名(デフォルト:X-CSRF-TOKEN)、CSRFトークンの値を設定する。 |
(6)
|
この書き方はXSSの可能性があるので、実際にJavaScriptコードを書くときは気を付けること。
今回の例では
data.addResult 、data.subtractResult 、data.multipyResult 、data.divideResult の全てが数値型であるため、問題ない。 |
JSONでリクエストを送信する場合も、同様にHTTPヘッダを設定すればよい。
Todo
Ajax対応する例がなくなっているため、例を直す。
6.7.2.4. マルチパートリクエスト(ファイルアップロード)時の留意点¶
Filter
では取得できない。CsrfFileter
がCSRFトークンを取得できず、不正なリクエストと見なされてしまう。そのため、以下のどちらかの方法によって、対策する必要がある。
org.springframework.web.multipart.support.MultipartFilter
を使用する- クエリのパラメータでCSRFトークンを送信する
Note
それぞれメリット・デメリットが存在するため、システム要件を考慮して、採用する対策方法を決めて頂きたい。
ファイルアップロードの詳細については、FileUploadを参照されたい。
6.7.2.4.1. MultipartFilterを使用する方法¶
Filter
内で取得できない。org.springframework.web.multipart.support.MultipartFilter
を使用することで、マルチパートリクエストでも、Filter
内で、Warning
MultipartFilter
を使用した場合、springSecurityFilterChain
による認証・認可処理が行われる前にアップロード処理が行われるため、
認証又は認可されていないユーザーからのアップロード(一時ファイル作成)を許容してしまう。
MultipartFilter
を使用するには、以下のように設定すればよい。
web.xmlの設定例
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class> <!-- (1) -->
</filter>
<filter>
<filter-name>springSecurityFilterChain</filter-name> <!-- (2) -->
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<servlet-name>/*</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
項番 | 説明 |
---|---|
(1)
|
org.springframework.web.multipart.support.MultipartFilter を 定義する。 |
(2)
|
springSecurityFilterChain より前に、MultipartFilter を定義すること。 |
JSPの実装例
<form:form action="${pageContext.request.contextPath}/fileupload"
method="post" modelAttribute="fileUploadForm" enctype="multipart/form-data"> <!-- (1) -->
<table>
<tr>
<td width="65%"><form:input type="file" path="uploadFile" /></td>
</tr>
<tr>
<td><input type="submit" value="Upload" /></td>
</tr>
</table>
</form:form>
項番 | 説明 |
---|---|
(1)
|
spring-mvc.xmlの設定の通り、
CsrfRequestDataValueProcessor が定義されている場合、<form:form> タグを使うことで、CSRFトークンが埋め込まれた<input type="hidden"> タグが自動的に追加されるため、JSPの実装で、CSRFトークンを意識する必要はない。
<form> タグを使用する場合
CSRFトークンを明示的に埋め込む方法でCSRFトークンを設定すること。
|
6.7.2.4.2. クエリパラメータでCSRFトークンを送る方法¶
認証又は認可されていないユーザーからのアップロード(一時ファイル作成)を防ぎたい場合は、
MultipartFilter
は使用せず、クエリパラメータでCSRFトークンを送る必要がある。
Warning
この方法でCSRFトークンを送った場合、<form:form method="GET" ...>...</form:form>
使用時と同様に、CSRFトークンがURLに現れるという問題がある。
以下に、CSRFトークンをクエリパラメータとして送る実装例を示す。
JSPの実装例
<form:form action="${pageContext.request.contextPath}/fileupload?${f:h(_csrf.parameterName)}=${f:h(_csrf.token)}"
method="post" modelAttribute="fileUploadForm" enctype="multipart/form-data"> <!-- (1) -->
<table>
<tr>
<td width="65%"><form:input type="file" path="uploadFile" /></td>
</tr>
<tr>
<td><input type="submit" value="Upload" /></td>
</tr>
</table>
</form:form>
項番 | 説明 |
---|---|
(1)
|
<form:form> タグのaction属性に、以下のクエリを付与する必要がある。?${f:h(_csrf.parameterName)}=${f:h(_csrf.token)} <form> タグを使用する場合も、同様の設定が必要である。 |