3分で分かるAngularJSセキュリティ
先日のng-mtg#4 AngularJS 勉強会でLTしようと思ったけど申し込みが間に合わなかったのでブログに書きます。
先月リリースされたAngularJS 1.2はセキュリティがんばってる的なことを聞いたので、セキュリティ周りの仕組みを調べてみました。
お題は以下です。
- CSRF
- JSON
- CSP (Content Security Policy)
- Escaping
CSRF
- ユニークなトークンをHTTPリクエストに載せてサーバーでチェックする対応が世の中では主流(最近はカスタムヘッダのチェックによる対策も)
- AngularJSでは、
XSRF-TOKEN
Cookieにトークンが載っていると、$http
を使ったHTTPリクエストのヘッダに自動的にX-XSRF-TOKEN
ヘッダーが付く。 XSRF-TOKEN
CookieはもちろんNot HttpOnlyで。- Angular界ではCSRFではなくXSRFで統一されてるのでgrepするとき注意。
- Cookie名とヘッダー名は変更もできるよ。
app.config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.xsrfCookieName = 'csrf_token'; $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token'; }]);
- 詳しくは AngularJS: ng.$http
JSON
JSON関連の攻撃手法は主にこの3つ。
- 直接ブラウズ
- JSONハイジャック
- UTF-7
参考: PHPのイタい入門書を読んでAjaxのXSSについて検討した(3)~JSON等の想定外読み出しによる攻撃~ - ockeghem(徳丸浩)の日記
- AngularJSでは、
)]}',
をJSONレスポンスの先頭に付ける手法が組み込みで用意されていて、これが付いてる場合は自動で除去してくれる。
)]}', ['one','two']
- このあたりは
$httpProvider.defaults.transformResponse
で実装されてる。詳しくは AngularJS: ng.$http - 欠点
- AngularJS以外のクライアントでは特殊な対応が必要
- devtoolなどでのデバッグが面倒
- これだけだと直接ブラウズ攻撃は防げない
おすすめはカスタムヘッダのチェック
- HTTPヘッダーに
X-Requested-With: XMLHttpRequest
を付加する。
app.config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.headers.common = {'X-Requested-With': 'XMLHttpRequest'}; }]);
- サーバーはAPIに対するリクエストについて
X-Requested-With
ヘッダがあることをチェックする。 - これで上記3つの攻撃を防げる。
CSP (Content Security Policy)
- 参考: CSP (Content Security Policy) - Security | MDN
- インラインスクリプト,
eval
,Function(string)
など脆弱なパーツの使用を禁止できる、対DOM Based XSS最終兵器。 - AngularJSでは
ngCsp
ディレクティブを指定するだけでクライアント側の対応は完了する。
<html ng-csp> ... </html>
- AngularJSはもともとインラインスクリプトを使わないのでCSPに向いている。
- ただし、高速化のために使用している
Function(string)
を無効化するので30%速度低下とのこと。 - CSSの注入もできなくなるので、
angular-csp.css
を別途ロードが必要。 - 詳細は AngularJS: ngCsp を参照。
個人的にはCSPはもっと普及して欲しいと思っているので、組み込みでこういう対応が入ってるのは好感持てます。
Escaping
各種テンプレートエンジンのデフォルトエスケープの普及によって単純なXSSは少なくなりましたが、DOM Based XSSはがんがん検出されている印象です。 理由としては次の2点があると思います。
- innerHTMLやそれに準ずるメソッド(jQuery系メソッドなど)への危険なHTMLのぶっ込み
- コンテキストが重なる場所でのエスケープの難しさ
コンテキストによるエスケープの難しさは Closure Templatesのオートエスケープが最強すぎる件 - teppeis blog で詳しく書きました。
簡単に書くと、x1, x2, x3で必要な処理が違うということです。
<!-- 疑似コード --> <a href="{$x1}" onclick="alert('{$x2}')">{$x3}</a>
x3
は普通にHTMLエスケープ、x1
はURLとしてサニタイズ(javascript:
などの不正なURLを排除)した上でHTMLエスケープ、x2
はJavaScript文字列としてエスケープした上でHTMLエスケープ、といった処理が必要。だけど分かんないよねーと。
AngularJS 1.2では$sce
がデフォルト有効に
- SCE = Strict Context Escaping
- AngularJS: ng.$sce
例えば、inputの中身をdivにHTMLとして出力する以下のコード。
<input ng-model="userHtml"> <div ng-bind-html="userHtml"></div>
$sceが無いとinputに<script>alert('xss')</scrip>
とか書いたらdiv.innerHTMLにぶっ込まれて実行されちゃうわけです。
これが$sceが有効な場合、HTMLコンテキストへの信頼できない文字列バインドとみなされてエラーになります。
HTMLとして文字列をバインドするには2通り方法があって、1つは信頼できるサーバーでサニタイズが完了してるときや文字列リテラルの場合。
そのときはJSから渡すときに$sce.trustAsHtml()
で文字列に信頼マークを付ける。いわゆる型付き文字列ですね。
app.controller('SecureCtrl', ['$scope', '$sce', function($scope, $sce) { $scope.userHtml = $sce.trustAsHtml('<b>安全だと分かっているHTML</b>'); }]);
もう1つはクライアントでサニタイズする方法。AngularJSにはHTMLサニタイズライブラリが付いてるのでそれを使う。ファイルが別なのでangular-sanitize.jsを読み込んでngSanitize
モジュールを登録。
<script src="angular.js"></script> <script src="angular-sanitize.js"></script>
var app = angular.module('app', [ 'ngSanitize']);
これだけで最初のng-bind-html
が動く。
プレーン文字列がHTMLコンテキストに代入されようとしているので、自動的にサニタイズされるわけです。
<script>alert('xss')</scrip><b>foo</b>
とか入力すると、サニタイズ後のキレイな<b>foo</b>
だけがHTMLとして代入される。
JSでやりたい場合は$sanitize
サービスを使う。AngularJS: ngSanitize.$sanitize を参照。
ng-bind-html
の代わりにng-bind-html-unsafe
を使うっていう方法もあるけど、これはテンプレートに穴を空けてるに過ぎないので非推奨。
$sanitize
の挙動
- https://github.com/angular/angular.js/blob/master/src/ngSanitize/sanitize.js
- 独自実装のDOM SAXパーサーでホワイトリスト式にフィルタリングする
- 触った感じでは、iframe, script, style要素、onclickみたいなイベントハンドラ属性、style属性は落ちた。
- href, src, backgroundなどの属性値はURLとしてバリデートされる。 URLのバリデータはAngularJS本体の$compileProviderのaHrefSanitizationWhitelistとかを使ってる。
- IE互換モードやIE7以下はCSS expressionが走っちゃうのでばっさり切り捨てられてて(ドキュメントで明言)、IE7以下で正しく動かないコードも入ってるので注意。
HTML以外のコンテキスト
以下の5つのコンテキストを持ってる。
$sce.HTML
$sce.URL
: a.href, img.src$sce.RESOURCE_URL
: img以外のiframeなどのsrc, ng-src, ng-includeなど$sce.JS
$sce.CSS
<a href="{{url}}">
とか書くとurl
は$sce.URL
としてサニタイズされて、javascript:alert(1)
とか書くとunsafe:javascript:alert(1)
に変換される。
$sce.RESOURCE_URL
はデフォルトでsame originに限定されてて、ng.$sceDelegateProviderのresourceUrlWhitelistで変更できる。
$sce.JS
と$sce.CSS
については、実は自動では使われる場所はないので使いたい人がJSで書いて明示的に使う。
AngularJS 1.2からonclick系のイベントハンドラ属性に直接代入できないので、上記のJSコンテキストの出番はほとんどなくなっている。
<!-- これはエラー --> <div onclick="{{foo}}">
あとsrc, ng-srcで複数の変数を連結して埋め込むのも1.2から禁止になった。分けわからなくなるからね。1個まで。
<!-- これもエラー --> <iframe src="{{baseUrl}}?a={{a}&b={{b}}">
ということで、エスケープ方法わかんないような複雑なコンテキストではそもそも変数埋め込みしない方がいいよね的な方向になってて、正しい気がします。
まとめ
3分厳しい!