Sequelize使ってますか?
この記事ではsequelizeのほんの1メソッドについて説明するので、sequelizeなんて名前も聞いたことないね。って方はおそらく、何の役にも立たない情報だと思います。そういう方はYoutubeでジャルジャルを見てください。めちゃくちゃ面白いですよ。
私のSequelize歴は浅く半年程度です。過去のプロジェクトからの引き継ぎで使ってるものであまり詳しくは理解していません。スマホアプリを作っていまして、私はバックエンドを担当しています。
ある日、こんな嫌な問題が起こりました。
アプリを社内リリースして、社長がガンガン使ってる時に「なんか、突然他人のデータが見れるようになって、自分のデータは見れなくなったんだけど。」 思い当たる節がなかったので勘も働かず、QAにバグを再現してもらうことになりました。 色々やってくれて、なぜか原因は分からないが1日半かけて同じバグを発生させてもらいました。
認証にはJWTのアクセストークン(とアクセストークンを更新するためのリフレッシュトークン)を使っているのですが、アクセストークンが他のユーザーのものになっていました。アクセストークンが期限が切れた後、リフレッシュトークンでアクセストークンを更新できたことから、リフレッシュトークンも他人のものだと判明しました。最悪
ですよね。 ユーザーの個人情報が思いっきりリークしてるんですよ。公にローンチする前だったのが、不幸中の幸いです。
大きな原因は2つ
-
jwtでトークンを発行すると常にユニークだと勘違いしていた。 トークンを発行する
jwt.sign()
は中身(ペイロードやオプション)が全く同じで、UNIX timestampが同じ場合はある特定のトークンを作るということです。
(参考)https://github.com/auth0/node-jsonwebtoken/issues/426 -
sequlizeのupsertはまずはCREATEを試みて、プライマリーキーが被ってる時、または特定のキーを引数で指定し被った時にUPDATEすると勝手に思い込んでいました。
仮に以下のようなテーブルがあったとします。
id(primaryKey, autoIncrement) | name | token(unique) |
---|---|---|
1 | Brasil | ABC |
2 | Peru | DEF |
3 | Colombia | GHI |
4 | Mexico | JKL |
5 | Peru | MNO |
6 | Brasil | OQR |
token
はunique制約を持っているとします。
await db.tableName.upsert({ name: "Argentina",value:"OQR" });
このようなコードが走ったときの、私の思い込んでた挙動
はtokenが被ってるので、新しくレコードを作れないとエラーを返すことです。
しかし、実際の挙動はガッツリ6行目のレコードをアップデートしにいきます。
(参考)https://sequelize.org/master/class/lib/model.js~Model.html#static-method-upsert
今回の事件は6行目のレコード変更前と変更後で説明がつきます。BrasilさんはOQRというトークンを持っていて、ほんの数100ms以内にトークンを発行したArgentinaさんにもOQRというトークンが発行されてしまい、しかもDBにUPDATEという形で書き込めてしまいます。次にBrasilさんがOQRというトークンを持ってきて、アクセストークンをアップデートしようとすると、DBにはOQRというトークンはArgentinaさんが結びついているので、Argentinaさん用のアクセストークンを発行してしまい、それをBrasilさんに返してしまいます。
解決策
リフレッシュトークンのペイロードにユーザーの固有情報を入れるか、n桁のランダム文字列を入れると、異なるユーザー間でトークンが被ることはなくなります。