Milkcocoaのセキュリティ機能を強化しました

Posted: / Tags: セキュリティ



Milkcocoaのセキュリティ機能をアップデートしました。この記事ではその詳細について説明します。

具体的には以下の機能が追加されました(されます)。

  1. データストアに保存できるデータの形式を制限できる
  2. APIを特定のオリジンでしか使えないようにできる(近日実装予定)
  3. 認証のためのトークンを自前で作成することができる

セキュリティ機能アップデートの経緯

アップデートするに至った理由は大きく2つあります。

認証を使用せずにセキュアにする手段がなかった

Milkcocoaのアプリには、それぞれ固有のapp_idが振り分けられています。

フロントエンド上でMilkcocoaインスタンスを作る際に、そのapp_idを入力することで、どのアプリへ操作するかを判断します(JavaScriptでは以下のようなコードになります)。

var milkcocoa = new MilkCocoa('app_id.mlkcca.com');

ただ、このコードをフロントエンドに書くということは、ソースさえ見てしまえば誰でもapp_idを知ることが出来てしまいます。

他人が、自分のapp_idを使って、自由にデータの保存や取得ができてしまいます。

本来は、認証機能を使ってアクセスに制限をかけるのですが、素早く簡単に作ったアプリに認証機能を実装しようとすると、余計な手間をかけることになってしまうという問題がありました。

これが一つ目の理由です。

デバイスの認証方法がない(簡易的な認証の実装方法がない)

「このRaspberry Piからだけpushを許可する」のような、IoTデバイスを簡単に認証する方法がありませんでした。

認証を実装をしようと思うと、Node.jsでAuth0でログインしてそのトークンをつかってMilkcocoaでログインをする、という面倒なことをしなければいけません。

認証方法がスマートではないし、Auth0に依存してしまっている、というのが2つ目の理由です。

この2つの理由から、今回のアップデートをするに至りました。

データストアに保存できるデータの形式を制限できる

保存するデータ形式のバリデーションを、データストア側で決められるようになりました。

Milkcocoaの1つのデータは、以下のようなオブジェクト形式になっています。

// milkcocoa.dataStore('データストアのパス名').push({key1: value1, key2: value2}); を実行した場合
{
  id: '(自動生成されるユニークな文字列)',
  timestamp: 12345....,
  value: {
    key1: value1,
    key2: value2
  }
}

このvalueの中身のデータの形式に、管理画面上のセキュリティルールで制限をかけることが出来るようになりました(セキュリティルールの基本的な使い方はこちらのドキュメントを参照下さい) 。

セキュリティルール上では、新しく追加されるデータのvalueにあたる部分を、newDataとして取得できます。(新しく追加されるデータとはpushset APIの呼び出し時のデータです)

newDataには以下のメソッドが使えます。

メソッド名説明
hasKey('key')引数で指定したkeyを持っていたらtrueを返す
isNumber()keyの値が数字だったらtrueを返す
isString()keyの値が文字列だったらtrueを返す
match()keyの値を正規表現で制限。マッチしたらtrueを返す

実際に、milkcocoaのトップページのドットお絵描きの場合、どういうセキュリティルールを書けば良いのか、以下に示します。

※セキュリティルールにコメントは書けないので、実際に記述するときはコメントは削除してください。

// dotsというデータストアには、'index'と'color'というkeyを持つデータしか受け付けない
dots {
    permit : push, on(push), query; // pushとqueryとonのみを有効に
    rule : newData.hasKey("index") && newData.hasKey("color");
}
// 'index'は数字、'color'は文字列でなければ受け付けない。
dots {
    permit : query, on(push);
    rule : true;
}
dots {
    permit : push;
    rule : newData.index.isNumber() && newData.color.isString();
}
// 'index'は0-2000の数字,'color'はカラーコードでなければ受け付けない。
dots {
    permit : push;
    rule : newData.index.match("[1-2]?[0-9]?[0-9]?[0-9]") && newData.color.match("^#([\da-fA-F]{6}|[\da-fA-F]{3})$");
}

カラーコードの正規表現はこちらの記事を参考にしました

これで、外部から形式の違うデータを保存しようとしても、はじくことができます。

APIを特定のオリジンでしか使えないようにできる(近日実装予定)

APIの使用を、特定のオリジンに限定できるようになりました。

オリジンといえば、HTMLやJavaScriptといったリソースの読み込み元のことで、日本語では「生成元」と訳されます(「同一生成元ポリシー」という言葉をどこかで見たことがあるのではないでしょうか)。

オリジンは、「プロトコル(httpやhttpsなど)」「ホスト(example.comやnews.example.comなど)」「ポート(:80や:8000など)」に分かれていて、そのうちのひとつでも異なれば違うオリジンとみなされます。(詳細はこちらの記事をご覧下さい)。

今までのMilkcocoaでは、管理画面の設定内の「許可Origin」で設定したオリジンは、認証機能のみにしか適用されていませんでした。

この「許可Origin」が、これからはすべてのAPIに適用されるようになります。

つまり、設定で指定したオリジン以外では、すべてのAPIが使えなくなってしまいます。

もし、現在使っているアプリで、許可Originを設定せずにAPIを使用している場合は、忘れずに設定するようお願いします(ネイティブアプリやNode.js等、オリジン(URL)を持たない環境であれば許可Originの設定はする必要はありません。今まで通りAPIは機能します)。

注意すべき点は、「オリジンをアクセス制御目的で使用してはいけない」という点です。

やろうと思えば、任意のオリジンを指定してアクセスすることが可能な上、ネイティブアプリやNode.jsなどオリジンが存在しない環境であればどこでもAPIが機能してしまうからです。

実際には、攻撃者はtelnet等で任意のOriginを送信可能。つまり、この実装例は脆弱であり、Origin:ヘッダを認証に使用してはいけない、ということになる。

それでも、とりあえずこのように指定しておけば、「別の場所にWebアプリがコピーされてもAPIが動いてしまう」ということはなくなります。

認証のためのトークンを自前で作成することができる

Milkcocoaのログインに使うトークンを自前で作成できるようになりました。

リアルタイムなToDoアプリをMilkcocoaとJavaScriptで作る(認証編)で、Auth0を使って認証機能を実装する方法を説明しました。

Auth0を使った認証が可能になるのはMilkcocoaがJSON Web Tokenに対応しているからで、トークンを作成する機構が他にあれば、Auth0を使わなくても認証を実装できます。

しかし、そもそもどうやって認証を実現しているのかいまいちわかっていない方も多いと思うので、簡単に説明します。

Auth0を使った場合の認証を図解します。

MilkcocoaのアプリのシークレットキーをAuth0側に教えることで、Auth0がそのシークレットキーをつかってログイン情報(生データ)をトークン化(暗号化)します。

そのトークンを使ってMilkcocoaでログインをすると、Milkcocoaのアプリがシークレットキーを使って解読して、解読できたらログイン情報をアプリに渡します。

つまり、「Milkcocoaのアプリのシークレットキーを使って、ログイン情報をトークン化する」ことができてかつ、「そのシークレットキーを誰にも知られないように」できれば、何でも良いということです。

Token Generator

そこで、自分でもトークンを作成できるAPIを用意しました。

Node.js製なので、npmでインストールして使います。

npm i milkcocoa-token-generator

tokenGeneratorインスタンスをつくったら、setSecret()で管理画面の「認証」にある、シークレットキーを入力します。

var TokenGenerator = require('milkcocoa-token-generator');
var tokenGenerator = new TokenGenerator();

tokenGenerator.setSecret('シークレットキー'); // このシークレットキーは絶対に他人に見られないようにしてください

シークレットキーを登録したら、generateTokenで、第一引数の生データをトークン化します。第二引数は、トークンの有効期限で単位は分です(デフォルトは20分)。

var token = tokenGenerator.generateToken({sub : 'device1'}, {expire : 30});

このトークンをつかって、authWithToken()でログインを行います。

milkcocoa.authWithToken(token, function() {
    var ds = milkcocoa.dataStore('path');
    ds.push({});
});

以下のようなセキュリティルールを書けば、上記のトークンでログインしたユーザーのみAPIが使えるようになります。

path {
  permit : all;
  rule : account.sub == 'device1';
}

このToken Generatorを使うことで、「あるデバイスからのみpushを許可する」といった、人以外の認証も可能になります。

またデバイスだけでなく、Token Generatorで作ったトークンをチーム内で共有することで、パブリックなURLでチーム内だけで使えるアプリを簡単に作成する、といったこともできます。

まとめ

Milkcocoaのセキュリティ機能強化について説明しました。

バリデーションについては、もう少し制限をかけられるように機能追加していく予定です。

許可Originについては、実装前に再度アナウンス致しますが、今のうちに設定して頂くおくことをおすすめします(設定をしないとAPIが動かなくなってしまいます)。

今後ともMilkcocoaをよろしくお願いします。