Spring SecurityにおけるCSRF保護の手引き

Spring SecurityのCSRF保護ガイド

1. 概要

このチュートリアルでは、クロスサイトリクエストフォージェリCSRF攻撃と、Spring Securityを使用してそれらを防ぐ方法について説明します。

参考文献:

Spring MVCとThymeleafによるCSRF保護

Spring Security、Spring MVC、およびThymeleafを使用したCSRF攻撃を防ぐための迅速かつ実用的なガイド。

スプリングブートセキュリティの自動構成

SpringBootのデフォルトのSpringSecurity構成に関する迅速で実用的なガイド。

Spring Method Securityの概要

Spring Securityフレームワークを使用したメソッドレベルセキュリティのガイド。

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保護を使用するには、まず、状態(PATCHPOSTPUT、および%)を変更するものすべてに適切な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ベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。