自己紹介
- opengl-8080
- 主に Qiita で技術メモを書いたり
- 関西の SIer 勤務
今日お話しすること
- Spring Security が保護してくれること、してくれないこと
- Spring Security を導入すれば、この攻撃は守ってくれる
- この攻撃は Spring Security の守備範囲外なので別途対処が必要
- 仕様や機能の話をメインにして、実装の細かい話は無し
デモ用アプリ
起動方法
GitHub からプロジェクトをダウンロードし、プロジェクトのルートで gradlew start
を実行
※初回は Payara (60MB)などのダウンロードが行われるので注意
# Windows
> gradlew.bat start
# Linux
$ ./gradlew start
動作確認
AP サーバーが起動したら、以下の URL にアクセスする。
-
https://localhost:8443/secure
- Spring Security を使用したアプリケーション
-
https://localhost:8443/non-secure
- 脆弱性のあるアプリケーション
同じ機能を持つアプリケーション(app
)に対して、脆弱性のある形で自力でセキュリティ機能を実装したもの(non-secure
)と、 Spring Security を導入したもの(secure
)を用意
ログインユーザ
-
user
とadmin
という2つのユーザを用意しています - パスワードはユーザー名と同じです
安全なウェブサイトの作り方
- IPA が公開している資料1
- ウェブサイトを作るときに注意すべきセキュリティ対策についてまとめられている
- ここで紹介されている個々の脆弱性に対して Spring Security がどうカバーしているか、という観点で説明していく
紹介されている脆弱性
- SQL インジェクション
- OS コマンド・インジェクション
- パス名パラメータの未チェック/ディレクトリトラバーサル
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- メールヘッダ・インジェクション
- クリックジャッキング
- バッファーオーバーフロー
- アクセス制御や認可制御の欠落
そもそも Spring Security の管轄外
- Spring Security は、 Servlet の Filter 機能を利用してセキュリティ機能を提供するフレームワーク
- HTTP リクエストの前後でごにょごにょする
- 根本的な対策が Filter でできないものは Spring Security の管轄外
- 例:SQL インジェクション
- 根本的な対策は、 SQL の組み立てにプレースホルダを使うこと
- Filter で完全に防ぐのは困難
管轄外の脆弱性を除く
SQL インジェクションOS コマンド・インジェクションパス名パラメータの未チェック/ディレクトリトラバーサル- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
メールヘッダ・インジェクション- クリックジャッキング
バッファーオーバーフロー- アクセス制御や認可制御の欠落
これらは Spring Security を導入しても、個別に対策を講じなければならない。
※管轄外だ、というのは私の判断によるものです(Spring Security のドキュメントに「そうだ」と書いているわけではないので、もし「~~すれば Spring Security でも防げるよ」というのがあれば教えていただけると助かります)
残った脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
残った脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
どういう問題?
- ユーザの識別で使用する識別子(セッションID)の扱い方に問題がある
- セッションID を盗まれたりすることで、別のユーザになりすますことができてしまう
- 情報の漏洩やデータの改ざんにつながる
- 「安全なウェブサイトの作り方」では、3種類の不備を紹介している
セッション管理の不備
- セッションIDの推測
- セッションIDの盗用
- セッションIDの固定化
セッション管理の不備
- セッションIDの推測
- セッションIDの盗用
- セッションIDの固定化
1. セッションIDの推測
- 例:1からの連番
- 次の ID を攻撃者が推測できると、別人のセッションID を使ってなりすましができる
根本的対策
- 推測できないランダムな値を生成する
- 暗号論的疑似乱数生成器を使う(Java だと
SecureRandom
を使う)
Spring Security のサポート
- セッションID の発行は Servlet の仕事なので、 Spring Security の管轄外
Servlet の発行するセッションID
- 仕様上は具体的な採番方法について言及されていない様子
The identifier is assigned by the servlet container and is implementation dependent.
(訳)
識別子は Servlet コンテナによって割り当てられ、それは実装依存です。
- 使用している Servlet コンテナごとに確かめる必要がありそう
- Payara (GlassFish) は、実装を見たところ、セッションID の一部に
SecureRandom
を使っていた
セッション管理の不備
- セッションIDの推測
- セッションIDの盗用
- セッションIDの固定化
2. セッションIDの盗用
- セッションID を、攻撃者が盗みやすい状態で取り扱っている
- 例1:セッションID をクエリパラメータにセットしている
- 例2:HTTP 通信にも Cookie を流してしまう
2. セッションIDの盗用
- セッションID を、攻撃者が盗みやすい状態で取り扱っている
- 例1:セッションID をクエリパラメータにセットしている
- 例2:HTTP 通信にも Cookie を流してしまう
セッションID をクエリパラメータにセット
https://xxx.com/foo/bar?jsessionid=xxxxxxxxxxxxxxx
- Cookie がサポートされていないクライアント(昔のガラケーとか)でもセッションの仕組みが働くようにするための代替策
- URL リライティングと呼ばれる
- Referer にセッションIDが載ってしまうので、セッションIDが盗まれやすくなる
根本的対策
- 常に Cookie を使い、クエリパラメータにセッションIDを載せる仕組みを使わない
Spring Security のサポート
- セッションIDの管理は Servlet の仕事なので、管轄外
- ただし、保険的な機能はある(後述)
Servlet のセッションID管理
- 仕様上 Cookie のサポートは必須
7.1.1 Cookies2
Session tracking through HTTP cookies is the most used session tracking mechanism and is required to be supported by all servlet containers.
(訳)
HTTP Cookie によるセッション追跡は、セッション追跡のメカニズムとして最もよく使われており、全ての Servlet コンテナによってサポートされる必要があります。
- URL にセッションIDを載せる方法は必須ではないが、 Cookie をサポートしないクライアントにも対応できなければならないことになっている
7.1.4 Session Integrity2
Web containers must be able to support the HTTP session while servicing HTTP requrets from clients that do not support the use of cookies.
To fulfill this requirement, Web containers commonly support the URL rewriting mechanism.(訳)
Web コンテナは Cookie の利用をサポートしないクライアントからの HTTP リクエストを処理するときに、 HTTP セッションをサポートできなければならない。
この要求を満たすため、 Web コンテナは通常 URL リライティングの仕組みをサポートします。
セッション追跡の変更方法
<session-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
- Servlet 3.0 からは、
web.xml
でセッション追跡の方法を指定できる -
<tracking-mode>
にCOOKIE
を指定すれば、 Cookie が使われるようになる -
URL
を指定すると URL リライティングが使用されるようになる
Spring Security によるガード
- Spring Security は URL リライティングが働かないように保護機能を提供している
- URL にセッションIDが含まれていると、自動的に除去される
- 意図せず URL リライティングが動作しても、セッションID がクエリパラメータに載らないようになっている
2. セッションIDの盗用
- セッションID を、攻撃者が盗みやすい状態で取り扱っている
- 例1:セッションID をクエリパラメータにセットしている
- 例2:HTTP 通信にも Cookie を流してしまう
HTTP 通信にも Cookie を流してしまう
- セッションID が載った Cookie を暗号化されていない HTTP で流してしまうと、盗聴される危険性がある
根本的対策
- Cookie の secure 属性を有効にする
- ブラウザはその Cookie を HTTPS 通信でしか送信しなくなる
Spring Security のサポート
- こちらもセッションIDの管理なので、 Spring Security は管轄外
Servlet での secure 属性の指定方法
<session-config>
<cookie-config>
<secure>true</secure>
</cookie-config>
</session-config>
-
web.xml
で指定する -
<cookie-config>
の<secure>
にtrue
を指定する
セッション管理の不備
- セッションIDの推測
- セッションIDの盗用
- セッションIDの固定化
3.セッションID の固定化
- ログインの前後でセッションIDを変更しない場合に問題になる
- ログイン前に攻撃者が取得したセッションIDを被害者に使わせ、被害者がログインしたあとで攻撃者が同じIDでアクセスすることでなりすましができる
デモ
セッションID がログイン前後で変わらない様子
- ログイン前
- セッションID は
5fbb4bcd29ecba2a37f7caa07302
- ログイン後
- セッションID は
5fbb4bcd29ecba2a37f7caa07302
のまま
セッション固定化攻撃の様子
- 攻撃者のブラウザでログインページにアクセスする
- ログイン前のセッションID(
5fea683aa1653ccb9f0962150ad6
)を入手する
- 被害者のブラウザに、何らかの方法で攻撃者が入手したログイン前のセッションIDをセットする
- 被害者は、セッションIDが強制されていることに気付かずにログインする
- これで、攻撃者が用意したセッションID(
5fea683aa1653ccb9f0962150ad6
)は被害者を識別するためのセッションIDとなる
- 攻撃者側のブラウザでホームにアクセスすると、ログイン処理していないのにホームページが表示される
- このとき、セッションIDは
5fea683aa1653ccb9f0962150ad6
を使っているので、被害者に成りすました状態でアクセスしていることになる
根本的対策
- ログインの前後でセッションIDを変更する
Spring Security のサポート
- Form ログインを使っている場合、 Spring Security はデフォルトでログイン時にセッションIDを変更してくれる
デモ
セッション固定化攻撃を仕掛けようとする
- 攻撃者がセッションID (
609c76875301d863f53cdf9d175d
) を入手する
- 攻撃者が入手したセッションID をブラウザにセットする
- ログインすると、セッションID が変更される(
60c229f9b2988231a31587dd1df6
)
- 攻撃者側のブラウザでホームにアクセスしても、ログイン画面に飛ばされる
セッション管理の不備 まとめ
- セッションID の生成や追跡は Servlet コンテナの役割なので、そちらの設定で対処する
脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
どういう問題?
- ユーザが入力したテキストを html にそのまま出力すると、 JavaScript を埋め込まれることでページの改ざんやセッションIDが盗まれたりする
根本的対策
- html に出力するすべての要素に対してエスケープ処理をする
- 例:
<script>
→<script>
Spring Security のサポート
- 根本的対策についてのサポートはなし
- html の出力は、主にレンダリング(テンプレート)エンジンの役割
- Thymeleaf なら、デフォルトでエスケープしてくれる
- JSP はエスケープなし
- JSTL の
<c:out>
を使っていればエスケープされる
- JSTL の
保険的対策
- レスポンスヘッダーを使用した保険的な対策はサポートされている
X-XSS-Protection
- 反射型 XSS を防ぐためのブラウザ機能を有効にするレスポンスヘッダ
- ブラウザによってはこの保護機能がデフォルトでは無効になっている場合がある
- このレスポンスヘッダをつけることで、ブラウザの保護機能を有効にできる
- Spring Security は、デフォルトでこのヘッダーをレスポンスに付与する
ブラウザのサポート状況
ブラウザ | Desktop | Mobile |
---|---|---|
Chrome | Yes | Yes |
Edge | Yes | Yes |
Firefox | No | No |
IE | 8+ | ? |
Opera | Yes | Yes |
Safari | Yes | Yes |
Android WebView | - | Yes |
Firefox が対応していないことについて情報を頂きました、ありがとうございます!
X-XSS-ProtectionがFirefoxで対応していないのは、より詳細に設定できる Content-Security-Poricy で unsafe-inline をすればいいから。
— Yuji@にゃーん (@YujiSoftware) 2017年11月18日
レガシーブラウザ互換のためだけなのものなので、実装しないようです。 #ccc_g4 #jjug_ccc
デモ
X-XSS-Protection が無効な場合
※以下の操作は X-XSS の対策がデフォルトでは無効なブラウザ(Edge とか)を使って試してください(Chrome はダメ)
- ログイン後
X-XSS
を選択する
- この画面は、クエリパラメータの
message
をそのまま HTML 上に出力している - メニューから飛んだ場合は
message=Hello+World!!
がパラメータとして設定されているので、「Hello World!!」と画面に出力される
- 「攻撃を再現」リンクをクリックすると、 GitHub に上げている静的 HTML ページに飛ぶ
-
/non-secure
と/secure
のそれぞれに同じ X-XSS 攻撃を仕掛けるリンクが用意されている-
message
に<script>
タグを仕込んでいる
-
- 上のリンクをクリックする
- X-XSS が成功して、ダイアログが表示される
X-XSS-Protection を有効にした場合
-
/secure
の方にログインし、「攻撃を再現」を開く
- 今度は下のリンクをクリックする
- スクリプトは実行されず変なエラー画面になった
- ブラウザのコンソールを見ると、
XSS
をブロックしたことを示すメッセージが表示されている
脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
どういう問題?
- 正規ログインしているユーザに罠のリンクを踏ませるなどして、意図しない処理を実行させる
デモ
- メニューの「CSRF」を選択する
- テキストボックスに適当な文字を入力して「Add」ボタンをクリックする
- 入力された文字列がサーバーに保存され、保存された文字列がリストとなって表示される
- 「攻撃を再現」リンクを開くと、 GitHub 上の CSRF 攻撃を仕掛けるためのページが開く
- 上の「Submit」ボタンをクリックする
- CSRF 攻撃が成功し、攻撃ページで指定されていた文字列(
CSRF Attack!
)がサーバーに記録され、一覧に表示される
根本的対策
- リクエストが正規のページからやってきたことを検証する
- セッションID とは別に推測困難な文字列(トークン)を生成し、ページに埋め込んでおく
- リクエストのたびにトークンも送るようにし、トークンの検証を行う
Spring Security のサポート
- Spring Security は、デフォルトでこの CSRF 対策が有効になっている
デモ
-
/secure
にログインし、「CSRF」のページを開く
- 「攻撃を再現」リンクから攻撃用のページに飛び、次は下の「Submit」ボタンをクリックする
- CSRF 攻撃が検知され、エラーページに飛ばされる
チェックが行われる条件
- リクエストの HTTP メソッドが
GET
,HEAD
,TRACE
,OPTIONS
以外 -
GET
メソッドで状態を更新するような実装をしているとチェックをすり抜けるので注意 - 一応、設定で変更可能
脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
どういう問題?
- ユーザ入力を HTTP レスポンスヘッダーに利用する場合、改行コードの入力を許しているとレスポンスの内容が意図しない形に書き換えられる危険性がある
- 改行コードは HTTP メッセージの各ヘッダーやボディを区切る役割を持つため、改行コードを任意に埋め込めるようにしていると HTTP メッセージ(レスポンス)を改ざんできてしまう
イメージ
仮に、次のようなレスポンスを返すアプリが存在したとする。
Aaa-Header: AAAAA<改行>
Bbb-Header: BBBBB<改行>
User-Input-Header: 【ここにユーザが入力した文字列を出力】<改行>
<改行>
HTTP Message Body
もしユーザ入力が次のようなテキストの場合、
xxx<改行>Xxxx-Header: xxxxxxxxxx
これをそのまま埋め込むと、レスポンスは下のようになる
Aaa-Header: AAAAA<改行>
Bbb-Header: BBBBB<改行>
User-Input-Header: xxx<改行>
Xxxx-Header: xxxxxxxxxx<改行>
<改行>
HTTP Message Body
根本的対策
- 「安全なウェブサイトの作り方」では、実行環境や言語が用意しているヘッダ出力用 API を使うよう促している
- Java の場合は Servlet の HttpServletResponse#setHeader(String, String) とかになる
Servlet の API は改行コードを消す?
- Javadoc や JSR の仕様書を眺めたが、改行コードの扱い方についての記述は見当たらなかった
- Servlet の仕様としては、改行コードをどうするかは決められていないっぽい
Servlet でのヘッダーの扱いについて、詳しい情報を教えていただけました!
↓のような事情なようです。
すまん。URLエンコードじゃなくて、MIMEエンコードだった(RFC2047)
— m6s (@megascus) 2017年11月18日
ていう感じみたいですね。Payaraの実装見てませんが。
— m6s (@megascus) 2017年11月18日
実際のところ
- 今回 Payara を使って動作を検証
- 値に改行コードがある場合は自動的に除去され、一行になった
- 名前に改行コードがある場合はヘッダーのセットが拒否された
デモ
- リンクの「Header Injection」を選択する
- 「Header-Value」に複数行の値を入力して「Submit」ボタンをクリック
- レスポンスヘッダーを確認すると、改行が除去され1行になって返されている
Spring Security のサポート
-
HttpServletResponse
がラップされ、ヘッダーに改行コードを含んだ文字を渡したときの動作が変わる - 改行コード(
\n
or\r
)が含まれると、IllegalArgumentException
がスローされる - そのままだとサーバーエラー(500 エラー)になるので、ヘッダーに値を出力する場合は事前に改行コードの有無をチェックしなければならない
デモ
- 「Header-Value」に改行をいれた状態で「Submit」をクリックする
-
IllegalArgumentException
が発生してサーバーエラーになる
- 「改行コードを明示的に除去する」のチェックを入れて「Submit」する
- サーバー側で改行コードが除外されたうえでレスポンスヘッダーにセットされるようになる
- 今度はエラーにならずレスポンスが返される
脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
どういう問題?
- 見えない
<iframe>
を仕込むことで、閲覧者が意図していないクリック操作をさせる攻撃
デモ
- メニューから「Click Jacking」を選択する
- 「Count up」ボタンをクリックする
- サーバーに記録しているカウントがインクリメントされ、画面表示も更新される
- 「攻撃を再現」リンクをクリックすると、クリックジャッキングを仕掛けた攻撃ページが開く
- 「Click」ボタンを何回かクリックする
- 元のページに戻って、画面を再描画させる
- すると、攻撃ページで「Click」ボタンをクリックした数だけカウントが上昇している
- 攻撃ページには、元ページが
<iframe>
として埋め込まれている -
opacity
で透明にして見えなくしている - 元ページの「Count up」ボタンと攻撃ページの「Click」ボタンがうまく重なるように位置を調整されており、「Click」ボタンの位置をクリックすると実は「Count up」ボタンがクリックされるようになっている
根本的対策
- 自分の Web ページを
<iframe>
で表示できないようにする - レスポンスヘッダに
X-Frame-Options
を設定することで可能になる
ブラウザのサポート(基本機能)
ブラウザ | Desktop | Mobile |
---|---|---|
Chrome | 4+ | Yes |
Edge | Yes | Yes |
Firefox | 3.6.9+ | Yes |
IE | 8+ | Yes |
Opera | 10.5+ | Yes |
Safari | 4+ | Yes |
Android WebView | - | Yes |
Spring Security のサポート
- デフォルトで
X-Frame-Options
がレスポンスヘッダに設定される
デモ
-
/secure
の方の「Click Jacking」ページから「攻撃を再現」リンクを開く
-
X-Frame-Options
にdeny
が指定されているため<iframe>
に元ページを表示できなくなっている
脆弱性
- セッション管理の不備
- クロスサイト・スクリプティング
- CSRF
- HTTP ヘッダ・インジェクション
- クリックジャッキング
- アクセス制御や認可制御の欠落
どういう問題?
- 適切にアクセス制御ができていない場合、本来閲覧できてはいけない情報を見られたり、できてはいけない操作を実行されたりするかもしれない
根本的対策
- 認証・認可の制御を適切に実装する
- パスワードログイン
- 権限によるアクセス制御
Spring Security のサポート
// "/login" へのアクセスは誰でも可
.antMatchers("/login").permitAll()
// "/admin" 以下のパスへのアクセスは admin 権限を持つユーザのみ可
.antMatchers("/admin/**").hasAuthority("admin")
// それ以外のアクセスは認証されていれば可
.anyRequest().authenticated()
- Ant 形式のパスや権限の指定が、宣言的な定義で記述できる
- 見落としがちなバイパス方法にも対応
- URL のパスパラメータの考慮など
デモ
パスのマッチングが不十分な場合
-
user
でログインしたうえでメニューから「Access Control」を開く
- 権限がないのでエラー画面に飛ぶ
- つづいて
admin
ユーザでログインしてから、もう一度「Access Control」を開く - すると、今度は権限があるので画面が表示される
- もう一度
user
でログインしなおして、また「Access Control」を開く - ここで、 URL の末尾に
;aaa
と追加して再アクセスする
- すると、権限がないはずなのに画面が表示できてしまう
...
@WebFilter(value="/*", filterName="authorizationFilter")
public class AuthorizationFilter implements Filter {
private static final Map<String, String> PATH_AUTHORITY_MAP;
static {
Map<String, String> map = new HashMap<>();
map.put("/admin.jsp", "admin");
PATH_AUTHORITY_MAP = Collections.unmodifiableMap(map);
}
...
private boolean needsToValidateAuthorization(HttpServletRequest request) {
String uri = request.getRequestURI();
return PATH_AUTHORITY_MAP.keySet().stream().anyMatch(uri::endsWith);
}
...
- キーに権限が必要なパス(
/admin.jsp
)、値に必要な権限(admin
)を持ったMap
を定義 -
HttpServletRequest#getRequestURI()
で取得した URL の末尾が権限チェックが必要なパスかどうかで認可のチェックが必要か判断している -
/admin.jsp;aaa
のようなパスパラメータが入った URL がくると条件にマッチしなくなり、認可のチェックがスルーされてしまう
Spring Security の場合
- 当然パスパラメータのことも考慮してチェックしてくれる