Spring SecurityのCSRF保護ガイド
1. 概要
このチュートリアルでは、クロスサイトリクエストフォージェリCSRF攻撃と、Spring Securityを使用してそれらを防ぐ方法について説明します。
参考文献:
Spring MVCとThymeleafによるCSRF保護
Spring Security、Spring MVC、およびThymeleafを使用したCSRF攻撃を防ぐための迅速かつ実用的なガイド。
2. 2つの単純なCSRF攻撃
CSRF攻撃には複数の形式があります。最も一般的なもののいくつかについて説明しましょう。
2.1. GETの例
ログインしたユーザーが特定の銀行口座“1234”に送金するために使用する次のGETリクエストについて考えてみましょう。
GET http://bank.com/transfer?accountNo=1234&amount=100
攻撃者が被害者のアカウントから自分のアカウント(“5678”)に送金したい場合は、被害者にリクエストをトリガーさせる必要があります。
GET http://bank.com/transfer?accountNo=5678&amount=1000
それを実現する方法は複数あります。
-
Link:攻撃者は、たとえば、転送を実行するために、このリンクをクリックするように被害者を説得することができます。
-
Image:攻撃者は、ターゲットURLを画像ソースとして持つ<img/>タグを使用する可能性があるため、クリックする必要はありません。 ページがロードされると、リクエストは自動的に実行されます:

2.2. POSTの例
メインリクエストがPOSTリクエストである必要がある場合-例:
POST http://bank.com/transfer
accountNo=1234&amount=100
次に、攻撃者は被害者に同様のことを実行させる必要があります。
POST http://bank.com/transfer
accountNo=5678&amount=1000
この場合、<a>も<img/>も機能しません。 攻撃者は次のように<form>を必要とします。
ただし、次のように、JavaScriptを使用してフォームを自動的に送信できます。
2.3. 実用シミュレーション
CSRF攻撃がどのように見えるかを理解したので、Springアプリ内でこれらの例をシミュレートしてみましょう。
簡単なコントローラーの実装から始めます-BankController:
@Controller
public class BankController {
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/transfer", method = RequestMethod.GET)
@ResponseBody
public String transfer(@RequestParam("accountNo") int accountNo,
@RequestParam("amount") final int amount) {
logger.info("Transfer to {}", accountNo);
...
}
@RequestMapping(value = "/transfer", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void transfer2(@RequestParam("accountNo") int accountNo,
@RequestParam("amount") final int amount) {
logger.info("Transfer to {}", accountNo);
...
}
}
また、銀行振込操作をトリガーする基本的なHTMLページも用意しましょう。
CSRF test on Origin
Transfer Money to John
これは、オリジンドメインで実行されているメインアプリケーションのページです。
単純なリンクを介したGETと、単純な<form>を介したPOSTの両方をシミュレートしたことに注意してください。
では、the attacker pageがどのようになるか見てみましょう。
このページは、異なるドメイン(攻撃者のドメイン)で実行されます。
最後に、元のアプリケーションと攻撃者のアプリケーションの2つのアプリケーションをローカルで実行し、最初に元のページにアクセスしましょう。
http://localhost:8081/spring-rest-full/csrfHome.html
次に、攻撃者のページにアクセスしましょう。
http://localhost:8081/spring-security-rest/api/csrfAttacker.html
この攻撃者のページから発信された正確なリクエストを追跡することで、問題のあるリクエストをすぐに見つけて、元のアプリケーションにアクセスし、完全に認証することができます。
3. Spring Securityの構成
Spring Security CSRF保護を使用するには、まず、状態(PATCH、POST、PUT、および%)を変更するものすべてに適切なHTTPメソッドを使用していることを確認する必要があります。 (t3)はGETではありません)。
3.1. Java設定
CSRF保護は、Java構成ではenabled by defaultです。 必要な場合は、引き続き無効にすることができます。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
}
3.2. XML構成
古いXML構成(Spring Security 4より前)では、CSRF保護はデフォルトで無効になっており、次のように有効にできました。
...
Spring Security 4.xから開始–CSRF保護はXML構成でもデフォルトで有効になっています。もちろん、必要に応じて無効にすることもできます。
...
3.3. 追加のフォームパラメータ
最後に、サーバー側でCSRF保護を有効にした状態で、クライアント側のリクエストにもCSRFトークンを含める必要があります。
3.4. JSONを使う
JSONを使用している場合、CSRFトークンをパラメーターとして送信することはできません。代わりに、ヘッダー内でトークンを送信できます。
まず、ページにトークンを含める必要があります。そのために、メタタグを使用できます。
次に、ヘッダーを作成します。
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
4. CSRF無効化テスト
これらすべてが整ったら、いくつかのテストを行います。
まず、CSRFが無効になっているときに簡単なPOSTリクエストを送信してみましょう。
@ContextConfiguration(classes = { SecurityWithoutCsrfConfig.class, ...})
public class CsrfDisabledIntegrationTest extends CsrfAbstractIntegrationTest {
@Test
public void givenNotAuth_whenAddFoo_thenUnauthorized() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
).andExpect(status().isUnauthorized());
}
@Test
public void givenAuth_whenAddFoo_thenCreated() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser())
).andExpect(status().isCreated());
}
}
お気づきかもしれませんが、基本クラスを使用して、一般的なテストヘルパーロジックであるCsrfAbstractIntegrationTestを保持しています。
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class CsrfAbstractIntegrationTest {
@Autowired
private WebApplicationContext context;
@Autowired
private Filter springSecurityFilterChain;
protected MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders.webAppContextSetup(context)
.addFilters(springSecurityFilterChain)
.build();
}
protected RequestPostProcessor testUser() {
return user("user").password("userPass").roles("USER");
}
protected String createFoo() throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(new Foo(randomAlphabetic(6)));
}
}
ユーザーが適切なセキュリティ資格情報を持っている場合、リクエストは正常に実行されました。追加の情報は必要ありません。
つまり、攻撃者は前述の攻撃ベクトルのいずれかを使用するだけで、システムを簡単に侵害できます。
5. CSRF対応テスト
それでは、CSRF保護を有効にして、違いを見てみましょう。
@ContextConfiguration(classes = { SecurityWithCsrfConfig.class, ...})
public class CsrfEnabledIntegrationTest extends CsrfAbstractIntegrationTest {
@Test
public void givenNoCsrf_whenAddFoo_thenForbidden() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser())
).andExpect(status().isForbidden());
}
@Test
public void givenCsrf_whenAddFoo_thenCreated() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser()).with(csrf())
).andExpect(status().isCreated());
}
}
ここで、このテストが異なるセキュリティ構成(CSRF保護が有効になっている構成)を使用する方法。
これで、CSRFトークンが含まれていない場合、POSTリクエストは単に失敗します。これは、もちろん、以前の攻撃がオプションではなくなったことを意味します。
最後に、テストのcsrf()メソッドに注目してください。これにより、RequestPostProcessorが作成され、テスト目的でリクエストに有効なCSRFトークンが自動的に入力されます。
6. 結論
この記事では、いくつかのCSRF攻撃と、Spring Securityを使用してそれらを防ぐ方法について説明しました。
このチュートリアルのfull implementationは、the GitHub projectにあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。