angular-marked を使う + XSS対策など

AngularJS の Markdown表示用 directive を使ってみる。

## bower

bower で依存入手

```
bower install --save angular
bower install --save angular-marked 
```

```json:bower.json
{
  ..省略..
  "dependencies": {
    "angular": "~1.4.5",
    "angular-marked": "~0.0.21"
  }
}
```

## 適当なフォームなどを作り動作確認

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Sample1</title>
</head>
<body ng-app="app">
  <h2>入力文字列</h2>
  <textarea ng-model="text" cols="80" rows="5"></textarea>
  <h2>変換前</h2>
  <div ng-bind="text"></div>
  <h2>変換済みMarkdown</h2>
  <div marked="text"></div>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/marked/marked.min.js"></script>
  <script src="bower_components/angular-marked/angular-marked.js"></script>
  <script>
app = angular.module('app', [
  'hc.marked'
]);
  </script>
</body>
</html>
```

![スクリーンショット 2015-09-04 18.08.43.png](https://qiita-image-store.s3.amazonaws.com/0/61997/8266043c-2a33-f5ea-5b0a-4ae791267020.png)


## XSS対策


しかし、これでは XSS 対策が行われていない。
以下では試しにbタグを使っているが、scriptタグなんかも問答無用で動作しちゃう。

![スクリーンショット 2015-09-04 18.07.44.png](https://qiita-image-store.s3.amazonaws.com/0/61997/39334663-758f-5d10-99a8-1743e186cfba.png)



Makredはデフォルトでは、HTMLタグのエスケープを行わない。これを対処するために以下の行をスクリプトに追加する。

```js
  markedProvider.setOptions({
    sanitize: true
  });
```

これを設定後は以下のようになる。

![スクリーンショット 2015-09-04 18.08.06.png](https://qiita-image-store.s3.amazonaws.com/0/61997/5ab2b3e3-fd3e-69fa-3a24-0543eb888605.png)

デフォルトでは以下のことが行われる

- escape処理
- `javascript:`から始まるリンクの無効化
- `vbscript:` から始まるリンクの無効化

デフォルトのエスケープ処理はこんな感じ

```js
function escape(html, encode) {
  return html 
    .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}
```

* [GitHub上ソースコードより転載](https://github.com/chjj/marked/blob/master/lib/marked.js#L1087)

このエスケープ処理は `markedProvider.setOptions()` で設定することができる `sanitizer` オプションにより自前のものに変更することができる。
何はともあれ、ユーザに何か入力させて表示させる場合は `sanitize: true` にしないとダメですね。

## 勝手に外の画像埋め込まれたくない時

Markdown では `![alt](URL)` による画像埋め込みに対応しているが、それを禁止したり、特定のドメインの画像に限定したいときは、画像タグの出力方法を変更することにより対処できる。

```js
app.config(['markedProvider', function(markedProvider) {
  renderer = new marked.Renderer();
  renderer.image = function(href, title, text) {
    return '';
  };
  markedProvider.setOptions({
    renderer: renderer,
    sanitize: true
  });
}]);
```

上の例では、image を出力しようとした時に、問答無用で空文字を返すように変更したもの。

もし、画像ファイルのソースURLのバリデーションを行いたい場合は元々のRenderの挙動を変更するとよいだろう。以下がオリジナルの挙動になる。

```js
Renderer.prototype.image = function(href, title, text) {
  var out = '<img src="' + href + '" alt="' + text + '"'; 
  if (title) {
    out += ' title="' + title + '"'; 
  }
  out += this.options.xhtml ? '/>' : '>'; 
  return out; 
};
```

* [GitHub上ソースコードより転載](https://github.com/chjj/marked/blob/master/lib/marked.js#L890)

というわけで、href をバリデーションばあいのコードが以下になる。

```js
app.config(['markedProvider', function(markedProvider) {
  renderer = new marked.Renderer();
  renderer.image = function(href, title, text) {
    if (href.indexOf('https://example.com/images/') === -1) {
      return ''; // https://example.com/images/ 下にない画像埋め込みはしない
    }
    var out = '<img src="' + href + '" alt="' + text + '"'; 
    if (title) {
      out += ' title="' + title + '"'; 
    }
    out += this.options.xhtml ? '/>' : '>'; 
    return out; 
  };
  markedProvider.setOptions({
    renderer: renderer,
    sanitize: true
  });
}]);
```

同じように、link などもよくわからんリンクを防いだり、クッションページ入れ込んだりなどなどいろいろカスタマイズが利いたりする。