71.2K Views
June 16, 24
スライド概要
JJUG CCC 2024 Springの発表資料です。
Webアプリケーションをクラッカーの攻撃から守るために様々な対策が求められます。例えばSQLインジェクション対策、CSRF対策、XSS対策、セッションID固定化対策などです。
このセッションでは、これらの対策について基礎から解説したあと、Spring Bootでどのように実装するかを解説します。
Java、Spring、IntelliJ IDEA
Webアプリケーション セキュリティの基礎と Spring Bootでの実装 JJUG CCC 2024 Spring 2024年6月16日 多田真敏 1
このセッションについて uWebアプリケーション開発時に セキュリティ観点で気をつけるべき点と、 それをSpring Bootでどう実装すべきかを解説します u網羅はしていませんのでご注意ください u必要な前提知識 u入門書レベルのSpring Boot・Spring MVC・Spring Security ・DBアクセス・HTML・JavaScript・HTTPの知識 2
自己紹介 u多田真敏(@suke_masa) uJJUG・JSUGスタッフ u金融事業会社で 社内システムの内製化+AWS化 uSpring研修・コンサルの副業を 考え中 uOSSドキュメントの和訳 uThymeleaf・Resilience4j 3
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 4
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 5
セキュリティ事故は大損害 u「企業が情報漏洩で賠償金◯◯億円」なんてニュースが ちょくちょくありますね😇 uユーザーにとっても大損害 u個人情報の漏洩 u意図しない多額の買い物、コメント投稿、情報の破壊 u・・・など 6
情報セキュリティの3大要素 u機密性 今回は主にこの2つ u認められた人だけアクセスできる u完全性 u破壊・改ざん・消去を防ぐ u可用性 u必要なときにアクセスできる 7
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 8
SQLインジェクションとは u入力項目にSQLの一部を含めることで、不正にログインしたり、 情報を取得したりすること ※説明のために敢えてtype="text"にしています。 現場では必ずtype="password"にしてください。 メールアドレス・パスワードが不正でも ログインできちゃった!? 9
発生のメカニズム // Javaプログラム String email = ""; String password = "' OR 1 = 1 --"; String sql = "SELECT email, name, password FROM users" + " WHERE email = '" + email + "'" + " AND password = '" + password + "'"; -- 実際に発行されるSQL SELECT email, name, password FROM users WHERE email = '' 最後の ' は AND password = '' OR 1 = 1 --' コメントアウトされる WHERE句全体でtrueになる 10
対策: エスケープする u' を '' (PostgreSQLの場合)に置き換える -- 実際に発行されるSQL SELECT email, name, password FROM users WHERE email = '' AND password = ''' OR 1 = 1 --' 「 ' OR 1 = 1 -- 」 というパスワードと 見なされる uエスケープすべき文字はRDB製品によって異なるので、 手動でのエスケープは漏れが起きやすい u次ページ以降の対策がオススメ 11
対策: プレースホルダーを使う
uSpringのJdbcClientの場合
public Optional<User> findByUsernameAndPassword(
String email, String password) {
String sql = """
SELECT email, name, password FROM users
WHERE email = :email AND password = :password
""";
Optional<User> userOptional = jdbcClient.sql(sql)
.param("email", email)
.param("password", password)
.query(new DataClassRowMapper<>(User.class))
.optional();
RDBがエスケープしてくれる
return userOptional;
→エスケープ漏れが起きない
}
12
対策: プレースホルダーを使う
uMyBatisの場合
@Select("""
SELECT email, name, password FROM users
WHERE email = #{email} AND password = #{password}
""")
public Optional<User> findByUsernameAndPassword(
String email, String password);
${}にすると
エスケープされない
ので注意
13
まとめ u値の埋め込みにはプレースホルダーを使いましょう uそもそも、SQLを文字列結合で作るのをやめましょう 14
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 15
XSSとは uCross-Site Scriptingの略 u悪い人がJavaScriptコードを含んだ内容を投稿 →その投稿を見た人のWebブラウザ上で JavaScriptが実行される uCookieの情報を抜き出して悪い人のサーバーに転送したりする 16
XSSの例 悪い人 ECサイトの商品レビュー画面 ①JavaScriptコードを 含んだレビューを投稿 ③いい人のブラウザ上で JavaScriptコードが実行 <script> alert('悪いコード実行!'); </script> いい人 ②レビュー画面にアクセス 17
対策: エスケープ u入力された値はエスケープしてから表示する uサニタイズとも呼ばれる u< は < に変換 u> は > に変換 18
対策: Thymeleafによるエスケープ
uThymeleafのth:text属性は出力前にエスケープしてくれる
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("message", "<script>alert('悪い処理')</script>");
return "hello";
}
<p th:text="${message}">メッセージ</p>
<p><script>alert('悪い処理')</script></p>
19
対策: JavaScriptによるエスケープ uざっとググった感じでは、React・Svelte・Vue.jsは デフォルトで出力をエスケープしてくれるっぽい u素人調査なのでご自分で必ず確認してください 20
対策: JavaScriptによるエスケープ
u生JavaScriptでは
uinnerHTMLプロパティはエスケープしない
utextContentプロパティはエスケープする
textContentで
指定したものは
リンクにならない
<div id="message1"></div>
リンク
<div id="message2"></div>
<a href="https://www.yahoo.co.jp/">リンク</a>
<script>
const div1 = document.getElementById('message1');
div1.innerHTML = `<a href="https://www.yahoo.co.jp/">リンク</a>`;
const div2 = document.getElementById('message2');
div2.textContent = `<a href="https://www.yahoo.co.jp/">リンク</a>`;
</script>
21
まとめ u入力された値は、必ずエスケープしてから出力しましょう! 22
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 23
デフォルトでは異なるオリジンにAjax不可 Webブラウザ api.sample.jpサーバー https://client.sample.jp オリジンが異なる fetch("https://api.sample.jp/...") ... ... リクエストを 送信できない 24
CORSとは uCross-Origin Resource Sharing u異なるオリジンにもAjaxリクエストを送信できるようにする u※オリジン=(スキーム、ホスト名、ポート番号)の組み合わせ uhttps://a.co.jp:443 と https://a.co.jp:443 は同じオリジン uhttps://a.co.jp:443 と http://a.co.jp:443 は異なるオリジン uhttps://a.co.jp:443 と https://b.co.jp:443 は異なるオリジン uhttps://a.co.jp:443 と https://a.co.jp:8443 は異なるオリジン 25
プリフライトリクエストとは uCORSできるか確認するために、 本来のリクエスト(以下「本リクエスト」)の前に ブラウザが自動送信するリクエスト uリクエストメソッドはOPTIONS u以下のリクエストヘッダーを付加する uOrigin: リクエストが発生したオリジン uAccess-Control-Request-Method: 本リクエストのメソッド uAccess-Control-Request-Headers: 本リクエストに含まれるヘッダー 26
プリフライトリクエストに対するレスポンス プリフライトリクエストに対する ステータスコードが2xx以外の場合は、 本リクエストを送信しない HTTP/1.1 200 OK access-control-allow-credentials: true access-control-allow-headers: content-type, x-csrf-token access-control-allow-methods: PUT,DELETE,GET,POST,PATCH,OPTIONS access-control-allow-origin: http://localhost:9080 access-control-expose-headers: * access-control-max-age: 7200 ... access-controll-xxxヘッダー がとても重要 27
プリフライトリクエストに対する レスポンスヘッダー ① access-control-allow-credentialsヘッダー utrueの場合、本リクエストでCookieや認証ヘッダーの送信を許可する ② access-control-allow-headersヘッダー u本リクエストで利用できるヘッダー一覧 ③ access-control-allow-methodsヘッダー u本リクエストで利用できるメソッド一覧 28
プリフライトリクエストに対する レスポンスヘッダー ④ access-control-allow-originヘッダー u本リクエストで利用できるオリジン ⑤ access-control-expose-headersヘッダー u本リクエストに対するレスポンスで公開できるヘッダー一覧 ⑥ access-control-max-ageヘッダー uプリフライトリクエストの結果をキャッシュできる時間(秒単位) 29
Spring BootでのCORS設定 uSpring MVCのWebMvcConfigurer実装クラスを Bean定義して、 addCorsMappings()メソッドをオーバーライドする 30
Spring BootでのCORS設定
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 適用対象のURL
// access-controll-allow-credentialsヘッダー
.allowCredentials(true)
// access-controll-allow-headersヘッダー
.allowedHeaders("Content-Type", "X-CSRF-TOKEN")
// access-controll-allow-methodsヘッダー
.allowedMethods("PUT", "DELETE", "GET", "POST", "PATCH", "OPTIONS")
// access-controll-allow-originヘッダー
.allowedOrigins("http://localhost:9080")
// access-controll-expose-headersヘッダー
.exposedHeaders("*")
// access-controll-max-ageヘッダー
.maxAge(7200); }}
31
Spring Security利用時 u前頁のaddCorsMappings()があればその設定が利用される uSpring Security独自の CorsConfigurationSourceというクラスでも設定可能 u個人的にはSpring MVCのaddCorsMappings()の方を使います (こっちの方が設定が何となく書きやすいから) u公式リファレンスはこちら 32
まとめ uCORSを許可するには、プリフライトリクエストに対して Access-Controll-xxxヘッダーを付加する必要がある uSpring BootではWebMvcConfigurerの addCorsMappings()をオーバーライドする 33
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 34
Cookieとは u以下の特徴を持つ、特殊なHTTPヘッダー uキーと値のペアを複数持つ u発行元サーバーからのSet-Cookieレスポンスヘッダーで 指定されたペアをブラウザが記憶する uブラウザは以降、発行元サーバーへリクエストする際、 Cookieリクエストヘッダーに記憶したペアを含める u各ペアには個別に有効期限などの属性を設定できる 35
Cookieの属性 ① Expires属性: Cookieの有効期限を指定 ② Secure属性: HTTPSでのみサーバーに送信される ③ HttpOnly属性: JavaScriptでの操作が不可 36
Cookieの属性 ④ Domain属性: Cookieを受信できるドメインを指定 uサブドメイン含む u未指定の場合は、発行元サーバーと同一ドメインの場合のみ送信される ⑤ Path属性: Cookieを受信できるパス(ドメインより後)を指定 37
Cookieの属性 ⑥ SameSite属性: ブラウザのURLとリクエスト先のURLが 同一かどうかによってCookie送信可否を決定 uLax(デフォルト) uブラウザのURLが発行元サーバーと異なる場合、GETでのみ送信 uブラウザのURLが発行元サーバーと同じ場合、制限なし uStrict: ブラウザのURLが発行元サーバーと同じだった場合のみCookieを送信 uNone: 送信先のURLに制限なし (ただしSecure属性も必要=HTTPS必須) 38
HttpSessionとは ujakarta.servlet.http.HttpSessionインタフェース u実装クラスはTomcatなどが持っている uクライアントごとにHttpSessionインスタンス(以下「セッション」)が 作られる u各HttpSessionインスタンスごとに一意なセッションIDを持つ u予測不能になるように、セッションIDはランダム値になっている uModel同様、値を保持できる u値の保持期限はセッション破棄またはタイムアウト(後述)まで → 複数回のリクエストにまたがって値を保持できる 39
Spring Bootでのセッション生成・破棄 uSpring Security + Tomcatの場合、 u初回アクセス時にセッションが生成される uログアウト時またはタイムアウト時にセッションが破棄される uデフォルトでは、30分間アクセスが無いとタイムアウト uJettyなど別のサーバーでは違う可能性あり(未検証) 40
クライアントをどうやって識別する? Aさん サーバー ①ログイン ③レスポンス ②インスタンス生成 ⑦リクエスト Aさんの セッション (ID=0001) サーバーはどうやって 「Aさんからのリクエスト」 と識別する? Bさん ④ログイン ⑥レスポンス ⑤インスタンス生成 Bさんの セッション (ID=0002) ※説明の単純化のためにセッションIDが連番になっています。 実際のセッションIDはランダム値になっています。 41
Cookieのjsessionidで識別する Aさん ①ログイン Set-Cookie: jsessionid=0001 サーバー ③レスポンス ②インスタンス生成 ⑦リクエスト Aさんの セッション (ID=0001) Cookie: jsessionid=0001 Bさん ⑤インスタンス生成 ④ログイン ⑥レスポンス Bさんの セッション (ID=0002) 42
Spring Bootでのセッション関連設定 (application.properties) # セッションタイムアウト時間 server.servlet.session.timeout=30m # セッションIDのCookie名 server.servlet.session.cookie.name=jsessionid # セッションID CookieのMaxAge属性 server.servlet.session.cookie.max-age=30m # セッションID CookieのSameSite属性 server.servlet.session.cookie.same-site=lax # セッションID CookieのSecure属性 server.servlet.session.cookie.secure=true # セッションID CookieのHttpOnly属性 server.servlet.session.cookie.http-only=true # セッションID CookieのDomain属性 server.servlet.session.cookie.domain= # セッションID CookieのPath属性 server.servlet.session.cookie.path=/ 特にSecure属性と HttpOnly属性は true必須 43
Spring SecurityでのHttpSession利用 uSpring SecurityはHttpSessionにユーザー情報を保存する u自動的に行われるので、我々が明示的に行う必要は無し AさんのHttpSession Aさんのユーザー情報 名前 = Aさん パスワード = ******** メールアドレス = [email protected] ... 44
HttpSessionへの値の保存・取得・削除 @GetMapping("/hello") public String hello(HttpSession session) { // 値の保存 session.setAttribute("名前", 値); // 値の取得 Object value = session.getAttribute("名前"); // 値の削除 session.removeAttribute("名前"); ... } 45
HttpSessionに値を保存する場合の注意点 u値はメモリ上に保存されるので、 値を大量に保存するとメモリを圧迫する可能性あり u可能な限りModelを利用する uModelに格納した値は、レスポンスを返したら必ず削除される → メモリを圧迫する可能性が低くなる u値が不要になったら必ず削除する uずっと残っているとメモリを圧迫し続ける 46
[参考] セッションで保持する値を メモリではなく外部ストレージに置く方法 uSpring Sessionを導入する uhttps://docs.spring.io/springsession/reference/3.0/index.html ロード バランサー u利用できる外部ストレージ uRedis uRDB Spring Boot Spring Boot uHazelcast uロードバランサーで複数サーバーに 分散されても安心! 外部 ストレージ 47
まとめ uSet-Cookieレスポンスヘッダーで指定されたペアは、 以降のCookieリクエストヘッダーに含まれる uCookieのjsessionidでクライアントを識別する ujsessionidにはSecure属性とHttpOnly属性を必ず付ける 48
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 49
セッションID固定化とは u悪い人のセッションIDを、いい人に使わせる攻撃 悪い人 サーバー ①初回アクセス ③Set-Cookie: jsessionid=0001 ⑦いい人として操作 jsessionid=0001 ②セッション生成 セッション (ID=0001) ④セッションID 0001 を使わせる いい人 ⑤ログイン jsessionid=0001 ⑥ログイン完了 50
対策: セッションID変更 uログイン後にセッションIDを変更(またはセッション再作成) → 悪い人が使わせたセッションIDから変更することで、 悪い人が悪さできなくなる 51
セッションID変更の効果 悪い人 ※説明の単純化のためにセッションIDが連番になっています。 実際のセッションIDはランダム値になっているので、 悪い人が新しいセッションIDを予測することは不可能です。 サーバー ①初回アクセス ③Set-Cookie: jsessionid=0001 ⑧いい人として操作 jsessionid=0001 ④セッションID 0001 を使わせる いい人 セッションIDが 間違っているので エラー ②セッション生成 セッション (ID=0001 →0002)≈ ⑥セッションID変更 ⑤ログイン jsessionid=0001 ⑦ログイン完了 Set-Cookie: jsessionid=0002 52
Spring Bootでの実装 uSpring Securityはデフォルトで、 ログイン後にセッションIDを変更する u設定で以下の挙動にも変更可能 ① セッションの再作成 ② セッションの再作成+セッションで保持した値のコピー ③ セッションの再作成もセッションID変更もしない 53
セッションに関する挙動の変更 @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain( HttpSecurity http) throws Exception { http.sessionManagement(sess -> sess .sessionFixation(fix -> fix .changeSessionId())) ... uchangeSessionId() : セッションID変更(デフォルト) unewSession() : セッションの再作成 基本的には デフォルトのままで OK umigrateSession() : セッションの再作成+セッションで保持した値のコピー unone() : 何もしない(基本的に非推奨) 54
まとめ uログイン後にはセッションIDを変更しましょう uSpring Securityがデフォルトでやってくれます 55
目次 ① セキュリティはなぜ重要か ② SQLインジェクション対策 ③ XSS対策 ④ CORS対策 ⑤ CookieとHttpSession ⑥ セッションID固定化対策 ⑦ CSRF対策 56
CSRFとは uCross-Site Request Forgeryの略 uいい人が意識しないうちに、 商品の購入やデータの変更などを行わせる 57
CSRFの流れ いい人 ①正常にログイン ②Set-Cookie: jsessionid=0001 いいサーバー good.com セッション (ID=0001) 悪いサーバー bad.com ③何らかの方法で悪いサーバーに誘導される ④悪いページをレスポンス(次のスライドへ) 58
CSRFの流れ
いい人のブラウザ
リクエスト先はgood.comなので、
Cookie: jsessionid=001
がヘッダーに付加されてしまう!
いいサーバー
good.com
⑤自動でformがsubmitされ、
いい人が気づかないうちに
購入処理が実行される
セッション
(ID=0001)
https://bad.com
悪いページ
悪いサーバー
bad.com
<!-- 画面には非表示 -->
<form method="post"
action="https://good.com/purchase">
</form>
59
対策: CSRFトークン uいい人しか知り得ないランダム値(=CSRFトークン)を いい人のブラウザと、いいサーバーのセッションで保持 → いいサーバーへのリクエスト時にCSRFトークンも送信 uPOST・PUT・DELETEなど、 データ変更の可能性がある処理のみCSRFトークンが必要 uGETでは必要なし 60
Spring Bootでの実装 uThymeleaf + Spring Security利用時は、 u初回アクセス時にCSRFトークンを作成し、セッションで保持 umethod="post"なformには、 CSRFトークンを保持するhiddenタグが自動で作成される uリクエストにCSRFトークンが含まれていない場合は403エラーになる いい人のブラウザにレスポンスされたHTML 実際はランダム値 <form method="post" action="/purchase"> ... <input type="hidden" name="_csrf" value="1234567890abcd"> <button>購入</button> </form> 61
CSRF保護対象コントローラーの単体テスト import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc public class PurchaseControllerTest { @Autowired MockMvc mvc; @Test @WithUserDetails(userDetailsServiceBeanName = "userDetailsService", value = "user") void purchaseTest() throws Exception { mvc.perform(post("/purchase") .with(csrf())) // CSRFトークンをリクエストパラメーターに含めてリクエスト .andExpect(status().isOk()) .andExpect(view().name("complete")); } } 62
Web APIの場合 uCSRFトークンを取得するWeb APIを作成して、 そこからCSRFトークンを取得 uPOSTなどのリクエスト時は、X-CSRF-TOKENヘッダーに CSRFトークンを指定する u無い場合は403エラー 63
CSRFトークン取得Web API
import org.springframework.security.web.csrf.CsrfToken;
@RestController
public class CsrfRestController {
@GetMapping("/api/csrf")
public CsrfToken getCsrfToken(CsrfToken csrfToken) {
return csrfToken;
}
{
}
"parameterName": "_csrf",
"headerName": "X-CSRF-TOKEN",
レスポンスされる
"token": "CSRFトークン値"
JSON
}
64
JavaScriptでCSRFトークン取得
const getCsrfToken = async () => {
const token = await fetch('/api/csrf', {
method: 'GET',
credentials: 'include',
}).then(async response => {
const json = await response.json();
if (response.ok) {
return json.token;
} else {
alert('Error');
CSRFトークンを取得
}
}).catch(error => {
alert('Error');
});
return token;
};
65
JavaScriptで購入処理
const csrfToken = await getCsrfToken();
fetch('/api/purchase', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify({
...
X-CSRF-TOKENリクエストヘッダーに
}),
CSRFトークンを指定
}).then(async response => {
...
66
CSRF保護対象コントローラーの単体テスト
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest @AutoConfigureMockMvc
public class PurchaseRestControllerTest {
@Autowired
MockMvc mvc;
@Test
@WithUserDetails(userDetailsServiceBeanName = "userDetailsService", value = "user")
void purchaseTest() throws Exception {
mvc.perform(post("/api/purchase")
.with(csrf().asHeader())) // CSRFトークンをリクエストヘッダーに含めてリクエスト
.andExpect(status().isOk())
.andExpect(content().json("""
{"status": "complete"}
"""));
}
}
67
Web APIの結合テストはやや面倒🫠 uテスト前に以下が必要 ① CSRFトークンと変更前のjsessionid取得 ② CSRFトークンと変更前のjsessionidを指定してログイン +変更後のjsessionid取得 u👆をリクエストヘッダーに設定してテストを実行する 68
例: Web APIの結合テスト
public record LoginResult(String sessionId, String csrfToken) {}
テストクラスには @SpringBootTest(
LoginResult login() {
webEnvironemnt = RANDOM_PORT)
// CSRFトークン取得
ResponseEntity<DefaultCsrfToken> csrfTokenResponse =
restClient.get().uri("/api/csrf")
.retrieve().toEntity(DefaultCsrfToken.class);
String csrfToken = csrfTokenResponse.getBody().getToken();
String sessionCookie = csrfTokenResponse
.getHeaders().get("Set-Cookie").getFirst();
// 変更前のjsessionidを取得
String oldSessionId = Arrays.stream(sessionCookie.split(";"))
.filter(element -> element.startsWith("JSESSIONID="))
.findFirst().get();
// 続く
69
例: Web APIの結合テスト
// ログイン
ResponseEntity<Void> loginResponse = restClient.post().uri("/login")
.body("...").contentType(MediaType.APPLICATION_JSON)
.header("Cookie", oldSessionId) // 変更前のjsessionidをヘッダーに指定
.header("X-CSRF-TOKEN", csrfToken) // CSRFトークンをヘッダーに指定
.retrieve().toBodilessEntity();
// 変更後のjsessionidを取得
String newSessionCookie = loginResponse
.getHeaders().get(HttpHeaders.SET_COOKIE).getFirst();
String newSessionId = Arrays.stream(newSessionCookie.split(";"))
.filter(element -> element.startsWith("JSESSIONID="))
.findFirst().get();
return new LoginResult(newSessionId, csrfToken);} // 続く
70
例: Web APIの結合テスト
@Test @DisplayName("購入できる")
void purchaseSuccess() {
// CSRFトークン取得+ログイン
LoginResult loginResult = login();
// テスト対象を実行
ResponseEntity<Void> response = restClient.post().uri("/api/purchase")
.body("...")
.contentType(MediaType.APPLICATION_JSON)
// 変更後のjsessionidをヘッダーに指定
.header("Cookie", loginResult.sessionId())
// CSRFトークンをヘッダーに指定
.header("X-CSRF-TOKEN", loginResult.csrfToken())
.retrieve()
.toBodilessEntity();
...
71
サーバー間通信の場合はCSRF対策不要 uブラウザを使わないから u以下の設定で、特定のURLでCSRFトークンを不要にできる @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain( HttpSecurity http) throws Exception { http.csrf(csrf -> csrf .ignoringRequestMatchers("CSRFトークン不要にしたいURL")) ... 72
まとめ uThymeleaf + Spring Security利用時は、 hiddenタグでCSRFトークンが自動で埋め込まれる uWeb APIの場合は ① CSRFトークンを取得するためのWeb APIを作成 ② X-CSRF-TOKENリクエストヘッダーにCSRFトークンを指定 uWeb APIのテストでは、テスト前にひと手間必要 uサーバー間通信の場合はCSRF対策不要 73
さいごに u通信は必ずHTTPSで暗号化しましょう! u証明書はロードバランサーに設定して、 ロードバランサー→サーバー間はHTTP、というのが典型的パターン 証明書 HTTPS HTTP Spring Boot HTTP Spring Boot ロード バランサー 74
さらに勉強するために u体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 uWebブラウザセキュリティ uマスタリングTCP/IP 情報セキュリティ編 第2版 uSpring Security Reference 75
ご清聴ありがとうございました! 76