AngularJSとサーバーサイドテンプレートの混在とngNonBindable

Angularとサーバーサイドテンプレートの混在

先日リリースされた某サービス(他社)がAngularを使っていて、XSSがボロボロ出てくるだとか、{{var}} な形式で値を入力するとng-template側でテンプレーティングされるだとかの話がありました。

詳しくは見ていないので、今回の話とまったく同じかは把握していませんが、サーバーサイドテンプレートを混在させると、次のようなことが起こりえます。

例えばejsとAngular

サンプルとしてスカスカなControllerを用意します。

angular.module('app', []).controller('AcmeCtrl', function($scope) {
  $scope.foo = 'bar';
});

ejsは次のようなテンプレートになっているとします。

<div ng-controller="AcmeController">
  <h2><%= title %></h2>
  <ul>
    <li ng-repeat="item in items" ng-cloak>{{item.name}}</li>
  </ul>
</div>

ejsにおける <%= title %> には {{foo}} という文字列が入っていたとして、それが処理されたた時点では次のようになります。

<div ng-controller="AcmeController">
  <h2>{{foo}}</h2>
  <ul>
    <li ng-repeat="item in items" ng-cloak>{{item.name}}</li>
  </ul>
</div>

ng-controller 配下にあるため {{foo}} が展開されて <h2>bar</h2> となります。

このとき、Angularも基本的にはHTMLエスケープをしてくれるので、即座に任意のスクリプトを挿入されるタイプのXSSになることはありませんが、スコープ内の値を変にお漏らしされてしまうと、大変に恥ずかしいものがあります。

また、保持はしたいけど画面に表示されて欲しくはない、際どい情報がうっかりとScope内で $parent など含めて辿れる位置に存在していても、困ったことになります。

スコープ内の関数を実行

では、次のようなパターンではどうでしょうか。

angular.module('app', []).controller('AcmeCtrl', function($scope) {
  $scope.foo = 'bar';
  $scope.onClick = function() {
    alert('hello!');
  };
});
<div ng-controller="AcmeController">
  <h2><%= title %></h2>
  <ul>
    <li ng-repeat="item in items" ng-cloak>{{item.name}}</li>
  </ul>
</div>

このとき、ejsの <%= title %>{{onClick()}} という文字列が入っていると、評価時に $scope.onClick() が実行されてalertが飛び出すことになります。前例と比べると、危険な感じになってきました。ユーザーの意志で行われるべきアクションが、第三者によって強制的に実行されてしまう可能性が出てきます。

回避策

今回の問題を回避する上で、最も望ましいのはAngularJSとサーバーサイドテンプレーティングを混在させない設計です。しかし、そうはなっていないケースもあることでしょう。そのような場合には、次のような回避策が考えられます。

ngNonBindable

フロントエンド的に、最も手っ取り早いのは ngNonBindable を利用することです。

<div ng-controller="AcmeController">
  <h2 ng-non-bindable><%= title %></h2>
  <ul>
    <li ng-repeat="item in items" ng-cloak>{{item.name}}</li>
  </ul>
</div>

先程の例に ng-non-bindable を加えた例です。こうすると、<h2 /> の中はAngularの評価から逃れられるため、<%= title %> にAngularが評価可能な文字列が入っていたとしても無視されます。

手っ取り早いのはよいのですが、結局、過度に混在したときに面倒くさい=人為的ミスで対応の抜け漏れが発生しやすいという点で、あまり良い手段とは言えません。

サーバーサイドでエスケープ

もうひとつの手段は、サーバーサイドテンプレートで出力する際に、HTML文字列のエスケープに加えて {{}} も何らかAngularが評価不能な形にエスケープしてしまうことです。

ちなみに、このときのInterpolate記号は、AngularJS: API: $interpolateProviderstartSymbolendSymbol として個別に変更することも可能です。

参考

サーバーサイドから本当に {{not-interpolate}} な文字列を出力したい場合に、Angularからの評価を逃れる手段がないとかエスケープがほんにゃらかんにゃらについて、詳しくは参考Issueにおける議論を見て下さい。