XSS攻撃対策についてNode.js Expressでアプリを構築して実例で理解する

## はじめに
Node.jsのExpressでテンプレートエンジンejsを使って実装するWebアプリを実例に、XSS攻撃を受ける脆弱性がある状態と対策を講じた場合の実装を見ていく事で、XSS攻撃について理解を深めてみようと思う。

## XSS（クロスサイト・スクリプティング）攻撃とは？
Webアプリケーションにスクリプト等を埋め込むことが可能な状態になっている＝脆弱性がある時に、これを利用されて利用者のブラウザ上で不正なスクリプトが実行されてしまう可能性がある。
そのスクリプト等を埋め込むような攻撃をXSS（クロスサイト・スクリプティング）攻撃という。

詳細は以下のサイトを参照。

https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_5.html

以下ではNode.jsのExpressでテンプレートエンジンejsを使った実装を例に、実際に脆弱性がある実装をやってみて、脆弱性がある時どのような事が起きるのか？またそれを防ぐためにどうするのか？をみていく。

見ていく内容としては、IPAのサイトに書かれている対策の一覧に書かれているものを順番に見ていくが、いくつか一般的なWebアプリでは実装する事は少なさそうなものは取り上げていない。

### 取り上げる内容

- HTMLテキストの入力を許可しない場合の対策
  - 【根本的解決】ウェブページに出力する全ての要素に対して、エスケープ処理を施す
  - 【根本的解決】URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する
  - 【根本的解決】\<script>...\</script> 要素の内容を動的に生成しない
  - 【保険的対応】入力値の内容チェックを行う
- 全てのウェブアプリケーションに共通の対策
  - 【根本的解決】HTTPレスポンスヘッダのContent-Typeフィールドに文字コード（charset）を指定する
  - 【保険的解決】Cookie情報の漏えい対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する
  - 【保険的解決】クロスサイト・スクリプティングの潜在的な脆弱性対策として有効なブラウザの機能を有効にするレスポンスヘッダを返す

### 今回は取り上げない内容

- HTMLテキストの入力を許可しない場合の対策
  - 【根本的解決】スタイルシートを任意のサイトから取り込めるようにしない
- HTMLテキストの入力を許可する場合の対策
  - 【根本的解決】入力されたHTMLテキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する
  - 【保険的対応】入力されたHTMLテキストから、スクリプトに該当する文字列を排除する

## 【根本的解決】ウェブページに出力する全ての要素に対して、エスケープ処理を施す
### 未対策だとどうなるか？
まずは、仮にこの対応しなかった場合どうなるか？だが、以下の動画のように

- クリックした時にJavaScriptを実行するようなリンク
- scriptタグそのもの

を仕込む事ができてしまう。
![ezgif.com-gif-maker (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/eb31363e-976a-c96a-b894-3a3046a489b7.gif)

動画のようになる場合の実装は以下。

```html:regist-confirm.ejs
	<body>
		...
					<div class="row mb-3">
						<label for="description" class="col-sm-2 col-form-label">
							XSS攻撃を受けるパターン
						</label>
						<div class="col-sm-10"><%- review.description %></div>
					</div>
					<div class="row mb-3">
						<label for="description" class="col-sm-2 col-form-label">
							XSS攻撃を受けないパターン
						</label>
						<div class="col-sm-10"><%= review.description %></div>
					</div>
		...
	</body>
```

ソースコード全体は以下から参照。

https://github.com/yuta-katayama-23/post-restaurant-reviews/commit/0335a00492c5b86e002005d05d98c6956b5685c2

### 対策を講じた場合どうなるか？
IPAが対応すべきと言っているように全てにエスケープ処理を施すをやってみる。が、実は[未対策だとどうなるか](#未対策だとどうなるか)で取り上げた実装の中にもエスケープをしている実装は既に入っており、

```
<div class="col-sm-10"><%= review.description %></div>
```

というのがエスケープをした時の実装。IPAのサイトにも書いてあるようにエスケープをすると、

> 特別な記号文字（「<」、「>」、「&」等）を、HTMLエンティティ（「&lt;」、「&gt;」、「&amp;」等）に置換する

という事が行われるので、HTMLの要素（\<a>や\<script>など）として扱われず単なる文字列になり無害化される。これによりXSS攻撃を防げる。

※ejsにおけるエスケープの実装方法は、[Embedded JavaScript templates　Features](https://github.com/mde/ejs#features)を参照。`<%= %>`はエスケープする書き方で、`<%- %>`はエスケープしない書き方。

※HTMLエンティティについては[Entity (エンティティ)](https://developer.mozilla.org/ja/docs/Glossary/Entity)を参照。

※今回は特に扱わなかったが、属性値を動的に生成する場合、その属性値は**必ず「"」（ダブルクォート）で括る＋「"」で括られた属性値に含まれる「"」をHTMLエンティティ「\&quot;」にエスケープする**という事も必須の対応になる。もしこれをしないと、以下のようにdataが`" onclick="悪意のあるスクリプト`だった場合、

```
<input type="button" value="<%- data %>">
  ↓
<input type="button" value="" onclick="悪意のあるスクリプト">
```

というように、悪意のあるスクリプトを埋め込まれたりする。ちゃんと対策（エスケープ）をしていれば、`" onclick="悪意のあるスクリプト`→`&quot; onclick=&quot;悪意のあるスクリプト`のようになるので無害化できる。（フロントエンドのフレームワークVue.jsだと[属性のバインディング](https://jp.vuejs.org/v2/guide/security.html#%E5%B1%9E%E6%80%A7%E3%81%AE%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0)に書かれているように、フレームワークでエスケープ処理をしてくれたりする場合もある）。

※SPAの世界（React、Vue、Angularなどで実装している場合）では、昔のようにサーバに値を送り、サーバ側でデータをHTMLに埋め込んでそれを描画するという事をしないのであまり問題にはならないと思われるが、例えばVueで言えば[セキュリティ](https://jp.vuejs.org/v2/guide/security.html)に書かれているような事を守らないと、XSS攻撃を受けるので注意が必要だろう。
Vue.jsではユーザの入力をそのままHTMLに描画する以下のような実装をすると脆弱性が生まれる。

```vue
new Vue({
  el: '#app',
  template: `<div>` + userProvidedString + `</div>` // 絶対にしてはいけない
})
```

- 参考：[実際にサンプルページで確認してみた！クロスサイトスクリプティング(XSS)の危険性！](https://eit.systems/danger-of-cross-site-scripting/)
- 参考：[クロスサイトスクリプティング(XSS)](https://www.tohoho-web.com/ex/xss.html)

## 【根本的解決】URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する
### 未対策だとどうなるか？
ここでもまずは脆弱性がある状態を実装してみる。URLを登録できるフォームがあるとして、何も対策をしなかった場合どうなるか？だが、以下の動画のようにURLとしてJavaScriptを仕込む事ができてしまう。このリンクをクリックすると、動画のようにJavaScriptが実行されてしまう。
![ezgif.com-gif-maker (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/809f6bc6-3e4b-9ee8-bb33-4b3ff2a83b9b.gif)

動画のようになる場合の実装は以下。

```html:regist-form.ejs
	<body>
    ...
					<div class="row mb-3">
						<label for="url" class="col-sm-2 col-form-label">URL</label>
						<div class="col-sm-10">
							<input
								type="url"
								class="form-control"
								id="url"
								name="url"
								autocomplete="off"
								pattern="https://.*"
								value="<%= url %>"
							/>
						</div>
					</div>
    ...
	</body>
```
```html:regist-confirm.ejs
...
					<div class="row mb-3">
						<label for="url" class="col-sm-2 col-form-label">URL</label>
						<div class="col-sm-10">
							<a href="<%= url %>"> <%= url %> </a>
						</div>
					</div>
...
```
```js:account.reviews.js
const createReviewData = (req) => {
	const { shopId, visit, score, description } = req.body;
	const date = moment(visit, DATE_FORMAT);

	const review = {
		shopId,
		score: parseFloat(score),
		visit: date.isValid() ? date.toDate() : null,
		post: new Date(),
		description
	};
	return review;
};

const validate = (req) => {
	const { visit, description } = req.body;
	const error = {};

	if (!visit) error.visit = '訪問日は必須です。';
	else if (moment(visit, DATE_FORMAT).isAfter(moment(new Date())))
		error.visit = '訪問日を未来の日付にする事はできません。';

	if (!description) error.description = '本文は必須です。';

	return error;
};

router.post('/regist/confirm', async (req, res) => {
	const error = validate(req);
	const { shopId, shopName, url } = req.body;
	const review = createReviewData(req);

	if (Object.keys(error).length !== 0) {
		res.render('./account/reviews/regist-form.ejs', {
			error,
			shopId,
			shopName,
			review,
			url
		});
	}

	res.render('./account/reviews/regist-confirm.ejs', {
		shopId,
		shopName,
		review,
		url
	});
});
```

ソースコード全体は以下から参照。

https://github.com/yuta-katayama-23/post-restaurant-reviews/commit/bfc0a2148c8a0eae300abc54e7fffa04b9d8798d

今回はエスケープをしていてもXSS攻撃は防ぐことはできない。また、[\<input type="url">](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/url)に書かれているように、` pattern="https://.*"`のように入力値にvalidationをかける事もできるが、これは動画のように簡単に書き換え可能なので対策としては完全ではない。

### 対策を講じた場合どうなるか？
IPAが対応すべきと言っているように、URLを出力する際にはhttpやhttpsに限定化する対策を見ていく。これはサーバサイド側で既に実装されている`validate()`関数を拡張すれば対応できる。つまり、このvalidate()でURLを検証し、http・httpsではないもの（javascriptなど）であればエラーを返すという処理を実装すればいい。

具体的には以下のような実装になる。今回は[valid-url](https://github.com/ogt/valid-url)を利用した。

```js:account.reviews.js
import { isHttpUri, isHttpsUri } from 'valid-url';

const validate = (req) => {
	const { visit, description, url } = req.body;
	...

	if (!isHttpUri(url) && !isHttpsUri(url)) error.url = 'URLが不正です。'; // ←これを追加

	return error;
};
```

このような実装をすれば以下の動画のように不正なURLを受け付けないように実装でき、URLを出力するときはhttp or httpsからのものに限定できので、XSS攻撃を防ぐことができる。
![ezgif.com-gif-maker (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/8565d22f-cd4e-217d-f3fa-ae7494c46c81.gif)

## 【根本的解決】\<script>...\</script> 要素の内容を動的に生成しない
### 未対策だとどうなるか？
脆弱性がある状態を実装してみる。入力された文字列に応じて動的にJavaScriptが変わるような実装をしている時、何も対策をしなかった場合どうなるか？だが、以下の動画のように入力文字を`</script><script>alert(1) //`のようにする事で、scriptタグの部分が閉じた事になり、攻撃者が実行したいとしてJavaScriptを仕込む事ができてしまう。
![ezgif.com-gif-maker (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/78cc9b0a-56c4-decd-8a2e-1394ab43f3d2.gif)

動画のようになる場合の実装は以下。

```html:regist-confirm.ejs
	<body>
		...
					<div class="row mb-3">
						<label for="description" class="col-sm-2 col-form-label">
							本文
						</label>
						<div class="col-sm-10">
							<textarea
								readonly
								class="form-control"
								id="description"
								name="description"
								rows="5"
							>
<%= (review.description || '') %>
</textarea
							>
							<div id="text-counter"></div>
						</div>
					</div>
		<script>
			var div = document.getElementById('text-counter');
			var txt = "<%- (review.description || '') %>";
			div.textContent = `${txt}の文字数は${txt.length}文字です`;
		</script>
		...
	</body>
```

ソースコード全体は以下から参照。

https://github.com/yuta-katayama-23/post-restaurant-reviews/commit/7d44bb6c4ad6b7276b3f24bb9a31ba098e8dc620

### 対策を講じた場合どうなるか？
これは『\<script>...\</script> 要素の内容を動的に生成しない』が対策になるので上記で取り上げたような実装をしない事が対策になる。

つまり、今回で言えばサーバサイドで`review.description`に表示される内容の文字数をカウントし、その数値を`<%= %>`でエスケープ処理をした上でHTML上に埋め込むという事をするべき。

※今回の実装で言えば、scriptタグ内でエスケープ処理をすれば、`</script><script>alert(1) //`が`&lt;/script&gt;&lt;script&gt;alert(1) //`のように無害化されるのでいいように思えるが、リスクを考えると、IPAに書かれているようにscriptを動的に生成しないようにすべきだろう。

## 【根本的解決】スタイルシートを任意のサイトから取り込めるようにしない
こちらについては実例は取り上げないが、「スタイルシートを任意のサイトから取り込めるように」について少し見ていく。
cssでは`expression()`を使ってJavaScriptを書けるが、このような実装を利用し、任意のサイトに置かれたスタイルシートを取り込めるような設計をしないようにする。

```css
width: expression ( document.body.clientWidth < 700 ? "700px" : "auto");
min-width: 700px;
```

ちなみに、上記は

- ユーザーが利用しているブラウザの画面幅が700px未満の場合：width :700px
- ユーザーが利用しているブラウザの画面幅が700px以上の場合：width :auto

にするというexpression関数。

※expression()関数を利用せずとも、以下のように\<head>\</head>内で外部からstylesheetを読み込んだり、\<script>\</script>でJavaScriptを読み込んだりする実装ができるが、これも任意のサイトからの取り込みと言えばそうであり、その中に悪意のあるものが混じらないとも限らないのでこうした実装にも実は注意が必要なのかもしれない。なのでwebpackのようなモジュールバンドラを使い、CDNで取り込むのではなくbuildして成果物が固定化されるようにする必要があるだろう。buildをしておけば少なくとも何か悪意のあるものが混入していても追跡可能にはなる。（実際、悪意のあるものが仕込まれたわけではないが、[OSS「faker.js」と「colors.js」の開発者、自身でライブラリを意図的に改ざん　「ただ働きはもうしない」](https://www.itmedia.co.jp/news/articles/2201/11/news160.html)のような事も現実に発生しており、注意が必要だろう）。

```html
<link
	rel="stylesheet"
	href="https://use.fontawesome.com/releases/v5.14.0/css/all.css"
/>
...
<script
	src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
	integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
	crossorigin="anonymous"
>
</script>
```

## 【保険的対応】入力値の内容チェックを行う
ここも詳細な実例は取り上げないが、どういうことか少し見ていく。この対策はあくまで保険的対応となり、根本的解決にならないので注意。

この対策は[URLを出力するときは、「http://」や 「https://」で始まるURLのみを許可する](#urlを出力するときはhttpや-httpsで始まるurlのみを許可する)で取り上げたようなvalidation（入力値の制限）をする方法。ただ、HTML上の属性値でvalidationをするような古典的な方法は書き換えできてしまうので無意味になる。また、フロントエンドのJavaScriptで入力値のチェックを行う事もできるが、IPAのサイトに書かれている通り、完全とは言えない（らしい）。

※一応具体的な実装も上げておくと、例えばemailか？の判定であれば、以下のように正規表現でチェックするとかが入力値の内容チェックにあたる。

```js
const el = document.querySelector(`#email`);
if (/[\w\d_-]+@[\w\d_-]+\.[\w\d._-]+/.test(el.value)) {
    ...
}
```

## HTMLテキストの入力を許可する場合の対策について
HTMLテキストそのもの（`<h1>The Crushing Bore</h1>`とか、`<p>By Chris Mills</p>`など）の入力を受け付けるWebアプリ自体が少ない気もするのでこのTopicについては本記事では省略する。

- 参考：[HTML テキストの基礎](https://developer.mozilla.org/ja/docs/Learn/HTML/Introduction_to_HTML/HTML_text_fundamentals)

## 【根本的解決】HTTPレスポンスヘッダのContent-Typeフィールドに文字コード（charset）を指定する
### 未対策だとどうなるか？
具体的に実装として見対策パターンは取り扱わないが、IPAのサイトに書かれているようにJavaScriptが実行されてしまう危険がある。

> たとえば、具体的な例として、HTMLテキストに、 「+ADw-script+AD4-alert(+ACI-test+ACI-)+ADsAPA-/script+AD4-」という文字列が埋め込まれた場合が考えられます。この場合、一部のブラウザはこれを「UTF-7」の文字コードでエンコードされた文字列として識別します。これがUTF-7として画面に表示されると 「<script>alert('test');</script>」として扱われるため、スクリプトが実行されてしまいます。

### 対策を講じた場合どうなるか？
WebアプリでHTMLを出力する際に想定した文字コードでHTMLがブラウザに解釈されるため、上記でIPAのサイトから引用したような`+ADw-script+AD4-alert(+ACI-test+ACI-)+ADsAPA-/script+AD4-`が`<script>alert('test');</script>`として解釈され、XSS攻撃を受けるという事がなくなる。

実際の実装はどうなるのか？だが、Expressであれば[res.render(view [, locals] [, callback])](http://expressjs.com/en/4x/api.html#res.render)でHTMLを返す（テンプレートエンジンでHTMLを生成してresponse bodyにセットする）を行えば、自動的に`Content-Type: text/html; charset=utf-8`になるので特に追加で実装が必要な事はない

※自動的にContent-Typeが設定されると言ったが、それはres.renderが実装されている[response.js](https://github.com/expressjs/express/blob/2585f209f98f91da68739bdb33b599df45b3a6e6/lib/response.js#L1013)で、(selfはthisなので)res.send()が実行されているが、res.sendの実装を見ると、[ここ](https://github.com/expressjs/express/blob/2585f209f98f91da68739bdb33b599df45b3a6e6/lib/response.js#L143)と[ここ](https://github.com/expressjs/express/blob/2585f209f98f91da68739bdb33b599df45b3a6e6/lib/response.js#L164)でレスポンスbodyがstringの場合（HMTLもstring）、Content-Typeを`text/html`に設定し、かつ`charset=utf-8`にする実装がされている。

※3.x系では自分でContent-Typeのcharsetを設定できる関数が実装されていたが、Expressは4.x系になってから`res.charset()`がなくなったので注意（[Other changes](http://expressjs.com/en/guide/migrating-4.html#other-changes)を参照）。

- 参考：[第1回　UTF-7によるクロスサイトスクリプティング攻撃［前編］](https://gihyo.jp/admin/serial/01/charcode/0001)
- 参考：[第2回　UTF-7によるクロスサイトスクリプティング攻撃［後編］](https://gihyo.jp/admin/serial/01/charcode/0002)

## 【保険的対応】Cookie情報の漏えい対策として、発行するCookieにHttpOnly属性を加え、TRACEメソッドを無効化する
### 未対策だとどうなるか？
以下の画像のように、簡単にJavaScriptからCookieの情報を抜き取れてしまう（このような状態だと、上記で見てきたような脆弱性があり、JavaScriptを実行されてしまうと、Cookieが外部に流出し、これがログインセッションに使われているのであればなりすましが成功してしまう）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/032a6124-8ecb-4a62-1cd7-deff91e40677.png)

上記のようになる場合の実装は以下。

```js:app.js
app.use(cookie());
app.use((req, res, next) => {
	const {
		cookies: { message }
	} = req;

	console.log(message);
	res.cookie('message', 'hello world!');

	next();
});
```

ソースコード全体は以下から参照。

https://github.com/yuta-katayama-23/post-restaurant-reviews/commit/006e421832528230e600aecfe99c1ee2877c0094

#### 対策を講じた場合どうなるか？
Expressであれば、res.cookie()にオプションが設定できるので、それをすればいい。実装としては以下のように変えるだけ。

```js
app.use((req, res, next) => {
	...
	res.cookie('message', 'hello world!', { httpOnly: true });
	...
});
```

- 参考：[Cookie へのアクセス制限](https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies#restrict_access_to_cookies)
- 参考：[Document.cookie](https://developer.mozilla.org/ja/docs/Web/API/Document/cookie)
- 参考：[res.cookie(name, value [, options])](http://expressjs.com/en/4x/api.html#res.cookie)

## 【保険的対応】クロスサイト・スクリプティングの潜在的な脆弱性対策として有効なブラウザの機能を有効にするレスポンスヘッダを返す
### 未対策だとどうなるか？
具体的に未対策の時の例は自明（上記のような脆弱性があった場合にXSS攻撃を受けやすくなる）なので取り上げないが、ブラウザに備わっているXSS（クロスサイト・スクリプティング）攻撃のブロックを試みる機能がユーザ設定により無効になる事があり、その場合利用者によってはXSS攻撃を受けやすくなってしまう。

### 対策を講じた場合どうなるか？
ExpressでIPAの資料通りに対応するのであれば以下のようにすればいい。

```js
...
app.use((req, res, next) => {
	res.set('X-XSS-Protection', '1; mode=block');
	res.set('Content-Security-Policy', 'reflected-xss block');
	next();
});
...
app.use('/public', express.static(appRoot.resolve('src/public')));
...
app.use('/', router);
...
```

※ポイントはHTMLを返す処理よりも上にmiddlewareを設定する事。そうしないとHTMLが返るような時全てで適用されない。詳細は[app.use([path,] callback [, callback...])](https://expressjs.com/ja/api.html#app.use)を参照。

> Middleware functions are executed sequentially, therefore the order of middleware inclusion is important.（ミドルウェアの機能は順次実行されるため、ミドルウェアを組み込む順番は重要です）

#### 補足　X-XSS-Protectionについて
今回はIPAの資料通りに対策を実装したが、実はこの辺りは議論があるようで特に`X-XSS-Protection`は`res.setHeader("X-XSS-Protection", "0");`のように設定すべきと言われている。具体的にどういうことか見ていくと、まずExpressには[Production Best Practices: Security](https://expressjs.com/en/advanced/best-practice-security.html)というページがあり、そこには[Helmet](https://github.com/helmetjs/helmet)の利用について書かれている（[Use Helmet](https://expressjs.com/en/advanced/best-practice-security.html#use-helmet)）。そのHelmetの[Reference](https://github.com/helmetjs/helmet#reference)を読むと、`helmet.xssFilter()`という部分があり、ここには、

> helmet.xssFilter disables browsers' buggy cross-site scripting filter by setting the X-XSS-Protection header to 0. See [discussion about disabling the header here](https://github.com/helmetjs/helmet/issues/230) and [documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection).（helmet.xssFilterは、X-XSS-Protectionヘッダを0に設定することで、ブラウザのバグであるクロスサイトスクリプティングフィルタを無効にします。 ヘッダの無効化に関する議論はこちらとMDNのドキュメントを参照してください。）

と書かれており、リンク先の[X-XSS-Protection: header should be disabled by default](https://github.com/helmetjs/helmet/issues/230)を読んでみると、要は「XSSフィルタを利用することで、むしろ悪用されてXS-Leak攻撃を引き起こす危険性があるので無効化した方がいい」という事らしい。なので、helmetの実装としては、

```js
import * as helmet from "helmet";
...
app.use(helmet.xssFilter());
```

となっている。

#### 補足　Content-Security-Policy（CSP）について
`Content-Security-Policy: reflected-xss block`だが、Chromeだと以下のようにエラーになるため設定できかった。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/de5dbf9a-38c6-a389-4ebc-8e6c535fc11c.png)
理由として、[CSP Directives: «Content Security Policy directive reflected-xss»](https://csplite.com/csp265/)に書かれているように、それは廃止されContent-Security-Policy（CSP）の仕様から削除されたtため（IPAのサイトは更新されていないのだろうか…）。

では何を設定すべきか？だが、これは実装によって変わってくるが、CSPの基本的な役割を理解しておけばカスタマイズして設定できると思われるので、少しCSPについてみていく。[コンテンツセキュリティポリシー (CSP)](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP)を見ると、

> an added layer of security that helps to detect and mitigate certain types of attacks（特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤー）

と書かれている。これだと正直良く分からなかったが、これをHTTPヘッダーに追加すると機能として何をしてくれるのか？というと、以下の通り。

- ユーザエージェント（ブラウザだと思ってよい）に読み込ませたいリソースの情報を指定する（取得するコンテンツのドメインを制限）<br>例えば、HTML上で以下のようにscriptやstyleshhetを読みこむことがあるかもしれないが、この時に読み込みを許可するドメインを設定する事ができ、これによりXSS攻撃の発生する箇所を削減・根絶できる（[クロスサイトスクリプティングの軽減](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP#mitigating_cross_site_scripting)を参照）

```
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.14.0/css/all.css" />
```
- 通信に使うプロトコルを指定する（[パケット盗聴攻撃の軽減](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP#mitigating_packet_sniffing_attacks)を参照）
- 違反内容の報告（[違反内容の報告](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP#enabling_reporting)を参照）

では実際にどのような設定をするとどうなるか？を具体的に見てみる。今回はExpressなので[Helmet](https://github.com/helmetjs/helmet)を使うと簡単なのでHelmetを利用する。Helmetをデフォルトの設定で利用すると、以下のように読み込めないコンテンツが出てくるが、これはCSPの機能として正しい動きになる（この後、Helmetの設定を修正して必要なコンテンツは読み込めるようにするが）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/70d7c257-aa73-3c89-f9c4-1ef9dc65ff6c.png)

上記のような画像の結果になる実装としては以下。

```js
import * as helmet from 'helmet';
...
app.use(helmet.contentSecurityPolicy());
```

エラーか所についてなんでこうなるのか？を見ていく。

- 緑色の枠で囲っている部分<br>デフォルトの設定`"script-src": ["'self'"]`により、自身のドメイン以外からはJavaScriptを読み込めない設定になっているためエラーになっている
- オレンジ色の枠で囲っている部分<br>は、デフォルトの設定`"upgrade-insecure-requests": []`によりコンテンツを読み込むときにはHTTPではなくHTTPSに変更されてしまい、今回はローカルなのでHTTPでサーバを立てているのでローカルのコンテンツが取得できずにエラーになっている

※デフォルトの設定については[Reference](https://github.com/helmetjs/helmet#reference)の`helmet.contentSecurityPolicy(options)`を開いた所に書かれている。また、CSPで設定できる内容については[Content-Security-Policy](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy)を参照。

上記のような状態だと必要なコンテンツも読み込めず困るので以下のようにHelmetの設定を変えると、画像のようにちゃんと読み込める（productionモードの時にはデフォルトの設定＋一部必要なJavaScript・CSSを読み込める状態になる事が望ましいと思われるので、その辺りの実装も本当は必要だと思われるが、今回はdevelopmentモードの時だけを想定した設定をしている）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/40d8a6f2-be0b-096d-5bae-e72ae03ac7c4.png)

```js
app.use(
	helmet.contentSecurityPolicy({
		useDefaults: false,
		directives: {
			'default-src': ["'self'"],
			'base-uri': ["'self'"],
			// 'block-all-mixed-content': [], <- MDNによると非推奨なのでコメントアウト
			'font-src': ["'self'", 'https:', 'data:', 'https://use.fontawesome.com'], // <- fontを読み込めるように追記
			'form-action': ["'self'"],
			'frame-ancestors': ["'self'"],
			'img-src': ["'self'", 'data:'],
			'object-src': ["'none'"],
			'script-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'], // <- JSを読み込めるように追記
			'script-src-attr': ["'none'"],
			'style-src': [
				"'self'",
				'https:',
				"'unsafe-inline'",
				'https://cdn.jsdelivr.net',
				'https://use.fontawesome.com' // <- cssを読み込めるように追記
			]
			// 'upgrade-insecure-requests': [], <- HTTPでコンテンツ取得したいのでコメントアウト
		}
	})
);
```

ソースコード全体は以下。

https://github.com/yuta-katayama-23/post-restaurant-reviews/commit/1fe3c14cc51f138e62f708baab82fbb2a6459263

※CSPの設定を行うと、デフォルトではscriptタグ内で以下のようなインラインスクリプトはブロックされ、そのscriptは実行されない。

```
<script>
	doSomething();
</script>
```

自身の実装したインラインスクリプト等で有効化したい場合には、`script-src`の設定に次のいずれかを追加する事で有効化できる。

- [unsafe-inline](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy#unsafe-inline)を追加する<br>これはインラインリソースの使用を許可するように設定できるものの、無条件ですべて許可してしまうのでXSS攻撃の対策としてはあまり良くないかもしれない
- [nonce-*](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy#nonce-)を追加する<br>サーバがレスポンスを返す度に毎回一意のnonce値を生成し、scriptタグにそのnance値を設定する方法。XSS攻撃対策としては強固だが、毎回サーバでnonce値を生成しなければならず面倒な気もする。

```
<script nonce="randumstring">
	doSomething();
</script>
```
- [sha*-*](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy#sha-)を追加する<br>これはscriptのJavaScriptコードのハッシュ値をsha256、sha384、sha512 のいずれかで取得し、それを`script-src`の設定に追加する方法。Chromeだと以下の図のようにエラーメッセージと共にハッシュ値を教えてくれたりする。コードのハッシュ値なので全く同じコードでない限り値が一致する事はないのでこれが手間を考えた時に現実的な対応な気もする（そもそもインラインスクリプトを書かなけれないいだけの話ではあるが）。![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/90f71920-7e36-4810-6028-2a41c20e5d46.png)

`sha*-*`をCSPのポリシーに追加した場合の（ExpressのHelmetでの）例は以下。

```js
app.use(
	helmet.contentSecurityPolicy({
		useDefaults: false,
		directives: {
			...
			'script-src': [
				"'self'",
				'https://cdn.jsdelivr.net/npm/',
				"'sha256-xVbLiF291eODYUjoJPH8GkxUoXzgyLSCbfdckFsRPMM='"
			],
			...
		}
	})
);
```

- 参考：[X-XSS-Protection](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-XSS-Protection)
- 参考：[Content-Security-Policy](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Security-Policy)
- 参考：[Express×Helmetでウェブセキュリティを学ぶ](https://qiita.com/qianer-fengtian/items/148602c437e1703aa764)
- 参考：[クロスサイトスクリプティングの軽減](https://developer.mozilla.org/ja/docs/Web/HTTP/CSP#mitigating_cross_site_scripting)
- [CSP Inline Scripts](https://content-security-policy.com/examples/allow-inline-script/)

## まとめとして
XSS攻撃に関して、攻撃の実例とその対策となる実装を見る事で理解を深める事ができたと思う。また、脆弱性としてはXSSが一番多く挙げられているようなのでこれはきちんと対策できるようにしたいと思った。

- 参考：[脆弱性対策情報データベースJVN iPediaの登録状況 [2021年第1四半期（1月～3月）]](https://www.ipa.go.jp/security/vuln/report/JVNiPedia2021q1.html)