1. 概要

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

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 –攻撃者は、たとえば転送を実行するために、このリンクをクリックするように被害者を説得することができます。
<a href="http://bank.com/transfer?accountNo=5678&amount=1000">
Show Kittens Pictures
</a>
  • 画像–攻撃者は、ターゲットURLを画像ソースとしてタグを使用する可能性があります。 言い換えれば、クリックすら必要ありません。 ページが読み込まれると、リクエストは自動的に実行されます。
<img src="http://bank.com/transfer?accountNo=5678&amount=1000"/>

2.2. POSTの例

メインリクエストがPOSTリクエストである必要があるとします。

POST http://bank.com/transfer
accountNo=1234&amount=100

この場合、攻撃者は被害者に同様のリクエストを実行させる必要があります。

POST http://bank.com/transfer
accountNo=5678&amount=1000

どちらもまた、 この場合、タグは機能します。

攻撃者は

<form action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="accountNo" value="5678"/>
    <input type="hidden" name="amount" value="1000"/>
    <input type="submit" value="Show Kittens Pictures"/>
</form>

ただし、フォームはJavaScriptを使用して自動的に送信できます。

<body onload="document.forms[0].submit()">
<form>
...

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ページも用意しましょう。

<html>
<body>
    <h1>CSRF test on Origin</h1>
    <a href="transfer?accountNo=1234&amount=100">Transfer Money to John</a>
	
    <form action="transfer" method="POST">
        <label>Account Number</label> 
        <input name="accountNo" type="number"/>

        <label>Amount</label>         
        <input name="amount" type="number"/>

        <input type="submit">
    </form>
</body>
</html>

これは、オリジンドメインで実行されているメインアプリケーションのページです。

実装したことに注意してください得る簡単なリンクと役職シンプルな

次に、攻撃者のページがどのようになるかを見てみましょう。

<html>
<body>
    <a href="http://localhost:8080/transfer?accountNo=5678&amount=1000">Show Kittens Pictures</a>
    
    <img src="http://localhost:8080/transfer?accountNo=5678&amount=1000"/>
	
    <form action="http://localhost:8080/transfer" method="POST">
        <input name="accountNo" type="hidden" value="5678"/>
        <input name="amount" type="hidden" value="1000"/>
        <input type="submit" value="Show Kittens Picture">
    </form>
</body>
</html>

このページは別のドメイン、つまり攻撃者ドメインで実行されます。

最後に、元のアプリケーションと攻撃者のアプリケーションの両方をローカルで実行してみましょう。

攻撃を機能させるには、ユーザーがセッションCookieを使用して元のアプリケーションに対して認証される必要があります。

まず、元のアプリケーションページにアクセスしてみましょう。

http://localhost:8081/spring-rest-full/csrfHome.html

ブラウザにJSESSIONIDCookieを設定します。

次に、攻撃者のページにアクセスしましょう。

http://localhost:8081/spring-security-rest/api/csrfAttacker.html

この攻撃者のページから発信されたリクエストを追跡すると、元のアプリケーションにヒットしたリクエストを見つけることができます。 JSESSIONID Cookieはこれらのリクエストとともに自動的に送信されるため、Springはそれらが元のドメインからのものであるかのように認証します。

3. SpringMVCアプリケーション

MVCアプリケーションを保護するために、Springは生成された各ビューにCSRFトークンを追加します。このトークンは、状態を変更するすべてのHTTPリクエスト(PATCH、POST、PUT、およびDELETE — )でサーバーに送信する必要があります。 GETではありません)。 攻撃者は自分のページからこのトークンを取得できないため、これによりアプリケーションがCSRF攻撃から保護されます。

次に、アプリケーションのセキュリティを構成する方法と、クライアントをそれに準拠させる方法を説明します。

3.1. Springセキュリティ構成

古いXML構成(Springセキュリティ4より前)では、CSRF保護はデフォルトで無効になっており、必要に応じて有効にすることができました。

<http>
    ...
    <csrf />
</http>

Spring Security 4.x以降、CSRF保護はデフォルトで有効になっています。

このデフォルト設定では、CSRFトークンが_csrfという名前のHttpServletRequest属性に追加されます。

必要に応じて、この構成を無効にすることができます。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .csrf().disable();
}

3.2. クライアント構成

次に、CSRFトークンをリクエストに含める必要があります。

_csrf 属性には、次の情報が含まれています。

  • トークン–CSRFトークン値
  • parameterName –HTMLフォームパラメーターの名前。トークン値を含める必要があります
  • headerName –HTTPヘッダーの名前。トークン値を含める必要があります

ビューでHTMLフォームを使用する場合は、parameterNametokenの値を使用して非表示の入力を追加します。

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

ビューでJSONを使用する場合は、headerNametokenの値を使用してHTTPヘッダーを追加する必要があります。

まず、メタタグにトークン値とヘッダー名を含める必要があります。

<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>

次に、JQueryを使用してメタタグ値を取得しましょう。

var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");

最後に、これらの値を使用してXHRヘッダーを設定しましょう。

$(document).ajaxSend(function(e, xhr, options) {
    xhr.setRequestHeader(header, token);
});

4. ステートレスSpringAPI

フロントエンドで使用されるステートレスSpringAPIのケースを確認してみましょう。

専用記事で説明されているように、ステートレスAPIにCSRF保護が必要かどうかを理解する必要があります。

ステートレスAPIがJWTなどのトークンベースの認証を使用する場合、CSRF保護は必要ないため、前に見たように無効にする必要があります。

ただし、ステートレスAPIがセッションCookie認証を使用する場合は、CSRF保護を有効にする必要があります 次に見るように。

4.1. バックエンド構成

ステートレスAPIは、HTMLビューを生成しないため、MVC構成のようにCSRFトークンを追加できません。

その場合、 CookieCsrfTokenRepository を使用して、CookieでCSRFトークンを送信できます。

@Configuration
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws {
        http
          .csrf()
          .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

この構成では、 XSRF-TOKENCookieがフロントエンドに設定されます。 HTTP-onlyフラグをfalseに設定したため、フロントエンドはJavaScriptを使用してこのCookieを取得できます。

4.2. フロントエンド構成

JavaScriptでは、document.cookieリストからXSRF-TOKENcookie値を検索する必要があります。

このリストは文字列として保存されるため、次の正規表現を使用して取得できます。

const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');

次に、APIの状態を変更するすべてのRESTリクエスト(POST、PUT、DELETE、PATCH)にトークンを送信する必要があります。

Springは、X-XSRF-TOKENヘッダーで受信することを想定しています。

JavaScript FetchAPIを使用して簡単に設定できます。

fetch(url, {
  method: 'POST',
  body: /* data to send */,
  headers: { 'X-XSRF-TOKEN': csrfToken },
})

5. 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)));
    }
}

ユーザーが適切なセキュリティクレデンシャルを持っている場合、リクエストは正常に実行されたことに注意してください。追加情報は必要ありません。

つまり、攻撃者は前述の攻撃ベクトルのいずれかを使用してシステムを危険にさらすことができます。

6. 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()メソッドは、テスト目的でリクエストに有効なCSRFトークンを自動的に入力するRequestPostProcessorを作成します。

7. 結論

この記事では、いくつかのCSRF攻撃と、SpringSecurityを使用してそれらを防ぐ方法について説明しました。

いつものように、この記事で紹介するコードは、GitHubから入手できます。