AngularJS の SCE 無効化

AngularJS の Strict Contextual Escaping を無効にする問題

説明

AngularJS の SCE(Strict Contextual Escaping)は、HTML や機密性の高い URL バインディングに、コンテキストに応じた信頼の検査を適用します。HTML の場合は、$sanitize があればサニタイズし、なければ信頼されていない値を拒否します。すべての CSS や JavaScript を自動でサニタイズする機能ではありません。$sceProvider.enabled(false) で SCE を全体で無効にすると、信頼できない値を ng-bind-html などに渡した際に XSS につながるおそれがあります。共通モジュールで無効にした場合も、アプリケーション全体に影響します。

AngularJS の公式サポートは 2022 年 1 月に終了しました。以下は既存の AngularJS アプリケーションを保守するための説明です。

想定される影響

  • XSS によりドキュメントのコンテキストで悪意のある JavaScript が実行され、画面の改ざんやさらなる攻撃につながるおそれがあります。
  • ページからアクセスできる Cookie やトークンを盗まれ、セッションを乗っ取られる可能性があります。
  • ページに表示された情報や、ページからアクセスできる個人情報、CSRF トークンなどが外部に送信されるおそれがあります。
  • DOM を改ざんして偽の入力フォームや決済画面を表示し、フィッシングに利用される可能性があります。
  • SCE の無効化と入力検証や CSP などの防御の不足が重なると、被害が拡大するおそれがあります。

対処方法

  • $sceProvider.enabled(false) を使わず、SCE が有効な既定の設定を維持してください。
  • 動的な内容は次のように扱ってください。
    • テキストには ng-bind を使用し、HTML として解釈させないでください。
    • HTML が必要な場合は、ngSanitize の $sanitize でサニタイズしてください。必要な場合だけ $sce.trustAsHtml(cleanHtml) を使い、そのコンテキストで信頼する値として設定してください。
  • コメントや自己紹介などのユーザー生成コンテンツは、サーバーとクライアントで許可リストに基づいてサニタイズしてから、保存または表示してください。
  • ng-bind-html に渡す値の取得元を制限し、信頼できるデータだけをバインドしてください。
  • サードパーティーのスクリプトやモジュールが SCE を無効にしていないか、コードレビューや静的解析(SAST)で確認してください。
  • Content-Security-Policy(CSP)を適用し、インラインスクリプトや任意のスクリプトの読み込みを制限してください。

例

変更前

各ブロックは、別々の app.js と index.html の抜粋をまとめて示しています。

javascript
<!-- app.js -->
(function(){
  angular.module('shopApp', [])
    .config(['$sceProvider', function($sceProvider){
      // BAD: SCE を全体で無効化
      $sceProvider.enabled(false);
    }])
    .controller('CommentCtrl', ['$location', function($location){
      var vm = this;
      // 攻撃者は URL ?c=<img src=x onerror=alert(1)> から注入可能
      vm.comment = $location.search().c || '';
    }]);
})();

<!-- index.html -->
<div ng-app="shopApp" ng-controller="CommentCtrl as vm">
  <!-- BAD: SCE を無効にした状態で HTML をそのまま描画 -->
  <div ng-bind-html="vm.comment"></div>
</div>

変更後

javascript
<!-- app.js -->
(function(){
  angular.module('shopApp', ['ngSanitize'])
    // SCE を無効にせず、既定の有効状態を維持
    .controller('CommentCtrl', ['$location', '$sanitize', '$sce', function($location, $sanitize, $sce){
      var vm = this;
      var raw = $location.search().c || '';
      // 1) 最も安全な選択肢: テキストとしてだけ表示
      vm.commentText = raw;
      // 2) 動的 HTML が必要な場合だけ、サニタイズしてから信頼する値として設定
      var clean = $sanitize(raw);
      vm.commentHtml = $sce.trustAsHtml(clean);
    }]);
})();

<!-- index.html -->
<div ng-app="shopApp" ng-controller="CommentCtrl as vm">
  <!-- HTML として解釈せずテキストとして描画 -->
  <div ng-bind="vm.commentText"></div>

  <!-- 必要な場合だけ使用: $sanitize + trustAsHtml の結果だけをバインド -->
  <div ng-bind-html="vm.commentHtml"></div>
</div>

説明:

  • 変更前: $sceProvider.enabled(false) で SCE の検査を無効にした状態で URL の入力を ng-bind-html に渡すため、危険な HTML が描画されるおそれがあります。
  • 変更後: SCE を維持し、通常は ng-bind でテキストを表示します。HTML が必要な場合は、先に $sanitize を適用します。$sce.trustAsHtml 自体はサニタイズする関数ではなく、値を信頼すると指定する API です。

参考資料