前回の投稿でMeteorを使って簡単なチャットアプリを作成してみました。
今回は少し視点を変えて、Meteorのセキュリティについて書いてみようと思います。
準備
今回の説明にあたり、前回のMyChatに修正を加えMyMemoというアプリを作ってみました。
簡単に言えば、一覧に表示されるコメントは投稿者自身にしか見えないようにしています。
つまり、今回は他人のコメントが見えたら重大なセキュリティ問題であるという前提です。
HTMLファイルは文言など若干の変更のみですので、JSファイルだけ公開しておきます。
- 登録時、コメントにUserIDも付加する
- データ取得時(commentヘルパー)にUserID指定してデータ抽出を行う
変更点は大きくこの二点です。
ChatComments = new Mongo.Collection('comments');
if (Meteor.isClient) {
Template.commentList.helpers({
'comment': function () {
var uid = Meteor.userId();
return ChatComments.find({userId:uid})
}
});
Template.inputForm.events({
'submit form': function (event) {
event.preventDefault();
var cmt = event.target.inputComment.value;
var eMail = Meteor.user().emails[0].address;
var uid = Meteor.userId();
ChatComments.insert({
commentStr:cmt,
userStr:eMail,
userId:uid
});
}
});
}
if (Meteor.isServer) {
}
このMyMemoには何点かセキュリティ上の問題があります。
それを説明した上でその回避方法について上記のコードに修正を加えながら説明していきます。
Meteorの仕組みについて
本題に入る前にMeteorの仕組みについて触れておきたいと思います。
MeteorのServerとClientの一体感のある動作は常時WebSocketで接続状態になっているという点も大きいですが、データの持ち方もポイントです。
MeteorはServerだけでなくClientにもComponentがあり、それらは自動的にServerからDBをClientにコピーしてきます。
Clientで動作するWebアプリケーションは、ServerではなくこのClientのCollectionに対してデータアクセスを行います。
Client側にアプリケーションの「飛び地」があるようなイメージですね
今回修正したMyMemoはClientのCollectionからユーザに紐づくデータのみを取得しています。
以下にこのアプリケーションにどういうリスクがあるのか説明していきます。
[リスク1] DB参照に関するリスク
MyMemoを表示している状態でまずはブラウザのコンソール画面を表示してください。
コンソール画面はChromeであれば右上にある「Google Chromeの設定」から「その他のツール」→「デベロッパーツール」で表示できます。
その上でコンソール画面上から以下のコマンドを入力してみてください。
ChatComments.find().fetch()
アプリケーション画面にはログインしているユーザのデータだけしか表示されていませんが、コンソール上には以下の通り他のユーザのデータ含め全てのDBの内容が見えてしまっています。
これは「ServerのDBのデータを全てClientにコピーしてしまっている」のが原因です。
そのため、関係無いユーザのデータまで取得してきてしまっています。
なぜこのようなことが起こるかというと、プロジェクトを作成した際にデフォルトでautopublishというパッケージが組み込まれるのですが、このパッケージは常にServerとClientのデータを完全同期するように動作しているためです。
このautopublish、プロトタイプを迅速に作成する場合には非常に便利なのですが、不要なデータまで取得してくるのはセキュリティ上問題があります。
正式なサービスを構築する場合には必要なデータだけをClientで保持するように修正を行いましょう。
まずは問題のautopublishパッケージを削除します。
meteor remove autopublish
これでServerからClientにデータがコピーされるということは無くなりましたが、データの同期が行われなくなったためアプリケーションの内容が全く表示されなくなってしまいました。
autopublishの代わりに必要なデータだけをコピーする仕組みが必要です。そのためにPublishとSubscribeという仕組みを使います。
Publish & Subscribe
まずはServer側でClient側に公開する情報を設定しましょう。
そのためpublishという機能を使います。
Publish
前回の投稿で全く使わなかったMeteor.isServer判定文以下に以下のコードを追加してください。
if (Meteor.isServer) {
Meteor.publish('privateMemos',function(){
var uid=this.userId;
return ChatComments.find({userId:uid})
});
}
Meteor.isServer判定文以下はServer側でしか動作しません。
データの抽出処理はisClientにあるcommentヘルパーとほぼ同じ処理をやっています。違いはClientのDBに対してかServerのDBに対してかということだけです。
上記のコードにより現在のユーザに紐づくデータだけを抽出し出力する機能が"privateMemos"という名称で作成されました。
###Subscribe
次にClient側で"privateMemos"を読み込む宣言を行います。
Meteor.isClient判定文以下に以下のロジックを追加してください。
if (Meteor.isClient) {
Meteor.subscribe('privateMemos');
これでClient側にはログイン中のユーザに紐づくデータのみコピーされるようになりました。
あと、一応commentヘルパーから検索条件を削除しておきましょう。
修正しなくても問題ありませんが、意味のないコードになってしまっています。
Template.commentList.helpers({
'comment': function () {
var uid = Meteor.userId();
return ChatComments.find()
}
});
この修正によって必要なデータのみClientにコピーするようになりました。
再度Meteorを起動し、再度DBの中身を見てみましょう。
ちゃんと自分のデータだけになりましたね。
このようにPublishとSubscribeを使うことでClientへコピーするデータをコントロールすることができます。
#[リスク2] DB更新に関するリスク
先ほどの修正でDBの参照については改善されましたが、まだ問題が残っています。
以下のコマンドをブラウザのコンソールで入力してみてください。
ChatComments.insert({
commentStr:'へへへ',
userStr:'xxx@zzz.com',
userId:'XXXXXXX'
});
DBの中身を見てみると特に問題は無さそうです。
ここでServer側のDBを見てみましょう。
Meteorを起動したままの状態で新規のコンソールを立ち上げ、以下のとおり入力してみてください。
meteor mongo
db.comments.find()
Server側のDBに「へへへ」というデータが挿入されています!
Client側からの不正な更新を許してしまったようです。
なぜでしょうか?
実は初期のプロジェクトではautopublishと同様に初期のプロジェクトにはinsecureパッケージ(名前からして危険ですね。。。)というものが組み込まれており、それによりDBに対してフルアクセスできるPermissionが設定されているのです。
参照の時と同様にこのパッケージを削除し、代わりの手段でDBの更新を行えるようにする必要があります。
まずはinsecureパッケージを削除しましょう。
やり方はautopublishと同じです。
meteor remove insecure
これでClientで直接DBの更新を行うことができなくなりました。
##method & call
次にDB更新機能をServer側定義し、Client側から利用できるようにします。
そのためにMeteor.methods内に機能を定義します。
今回は'insertMemo'という名称で機能を定義しました。
以下のコードを追加してください。
if (Meteor.isServer) {
Meteor.methods({
'insertMemo':function(cmt,eMail){
var uid = Meteor.userId();
ChatComments.insert({
commentStr:cmt,
userStr:eMail,
userId:uid
});
}
});
今度はClient側を修正します。
Server側で定義したinsertMemoを使用するためにMeteor.call関数を使用します。
submit formイベントの処理を以下の内容に置き換えます。
'submit form': function (event) {
event.preventDefault();
var cmt = event.target.inputComment.value;
var eMail = Meteor.user().emails[0].address;
Meteor.call('insertMemo',cmt,eMail);
}
修正完了後、再度実行してみましょう。
コンソールからの入力は失敗していますが、フォームからの登録は正しく行われています。
以下が最終版のコードとなります。
ChatComments = new Mongo.Collection('comments');
if (Meteor.isClient) {
Meteor.subscribe('privateMemos');
Template.commentList.helpers({
'comment': function () {
var uid = Meteor.userId();
return ChatComments.find()
}
});
Template.inputForm.events({
'submit form': function (event) {
event.preventDefault();
var cmt = event.target.inputComment.value;
var eMail = Meteor.user().emails[0].address;
Meteor.call('insertMemo',cmt,eMail);
}
});
}
if (Meteor.isServer) {
Meteor.methods({
'insertMemo':function(cmt,eMail){
var uid = Meteor.userId();
ChatComments.insert({
commentStr:cmt,
userStr:eMail,
userId:uid
});
}
});
Meteor.publish('privateMemos',function(){
var uid=this.userId;
return ChatComments.find({userId:uid})
});
}
#最後に
今回は、Meteorのセキュリティついてまとめてみました。
デフォルトで登録されているパッケージ群はプロトタイプ開発時には非常に有効ですが、正式サービス時にはセキュリティリスクの原因となりますので注意してください。
この2回は真面目にMeteorの仕組みをまとめてみましたが、次回はMeteorを使って遊んでみる予定です。