久々にfirestore.rules
のことで記事を書きたいなと思い、
今回はfirestore.rules
でupdate
の条件に焦点を当てて、書く時に気をつけたいことを書いてみます。
何に気をつけるべきか
もちろん、「どのユーザーがupdate
を実行できるか」を制御するのも大事です。(制御しないと他者が勝手に更新をして悪意ある情報に書き換え可能ですよね。)
それともう一つ大事なのが、「どのフィールドは書き換え不可なのか」です。
「適切な権限を持つユーザーがupdate
を実行できる状態」イコール、「更新によって適切なデータに更新される」にはならない場合があります。
次のようなドキュメントを例にして説明してみます。
post
という、記事を表すドキュメントで、タイトル、内容、作成者、作成日、更新日の情報を持っています。
作成時(create)には全ての情報が書き込まれ、書き込みを行うユーザーの、認証情報であるuidと、authorID
が一致しているときにcreate
できるものとします。
// '/posts/xxxxxx' のドキュメント
const post = {
title: 'Cloud Firestoreのruleで`update`の条件を書く時に気をつけたいこと',
body: '今回は`firestore.rules`で`update`の条件に焦点を当てて、書く時に気をつけたいことを書いてみます。 ...' ,
authorID: 'yyyyyy', // `/users/yyyyyy`のドキュメントのID、
createTime: '2019-05-28T09:00:00.000Z',
updateTime: '2019-05-28T15:00:00.000Z'
}
これに対応するfirestore.rules
を書いてみます。
update
以外の操作や、フィールドの値のバリデーションは省略します。
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postID} {
allow update: if incomingData().authorID == request.auth.uid;
}
}
function existingData() {
return resource.data;
}
function incomingData() {
return request.resource.data;
}
}
このように記述すると、「authorID
と認証ユーザのUIDが一致していたら更新が可能」ということになり、本人のみが更新可能であるように見え、
ルールとしても正しいように見えます。しかしこれだけでは不十分です。
何が不十分だったのか
これだけでは不十分の理由としては、
- authorIDを書き換えることが可能になってしまう。
- 更新の結果、authorIDが、更新操作を行ったユーザーのIDと一致すればいいので、乗っ取りが可能になる
-
createTime
を書き換えることができてしまうので、新着順で掲載するようなロジックがある場合に、新着をずっと占拠することが可能になる
といった点があげられます。
特に問題なのは、一見本人のみが操作ができるように見えて、実質他人が書き換え出来て乗っ取りが可能である点です。これは非常にまずいです。
incomingData().authorID
(==request.resource.data.authorID
)は、update
の操作を行った時に、最終的にこうなるという結果の値が入ってくるので、
// '/posts/xxxxxx' のドキュメント(変更前)
const post = {
title: 'Cloud Firestoreのruleで`update`の条件を書く時に気をつけたいこと',
body: '今回は`firestore.rules`で`update`の条件に焦点を当てて、書く時に気をつけたいことを書いてみます。 ...' ,
authorID: 'yyyyyy', // `/users/yyyyyy`のドキュメントのID
createTime: '2019-05-28T09:00:00.000Z',
updateTime: '2019-05-28T15:00:00.000Z'
}
// '/posts/xxxxxx' のドキュメント(変更後)
const post = {
title: 'Cloud Firestoreのruleで`update`の条件を書く時に気をつけたいこと',
body: '今回は`firestore.rules`で`update`の条件に焦点を当てて、書く時に気をつけたいことを書いてみます。 ...' ,
authorID: 'zzzzzz', // `/users/zzzzzz`のドキュメントのID
createTime: '2019-05-28T09:00:00.000Z',
updateTime: '2019-05-28T15:00:00.000Z'
}
となった場合に、incomingData().authorID
はzzzzzz
を指すことになります。これと、操作しているユーザーのUIDがzzzzzz
だった場合にupdate
の操作が許可されてしまいます。恐ろしい...
ルールを修正する
この問題を修正するには、
-
authorID
が、更新前後で変化しないこと -
createTime
が更新前後で変化しないこと
を条件に加えてあげます。
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postID} {
allow update: if incomingData().authorID == request.auth.uid
&& incomingData().authorID == existingData().authorID //追加
&& incomingData().createTime == existingData().createTime; //追加
}
}
function existingData() {
return resource.data;
}
function incomingData() {
return request.resource.data;
}
}
こうすることで、別のauthorIDに書き換えられたり、乗っ取られたりすることを防ぐことができます。
これじゃだめなの?
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postID} {
allow update: if existingData().authorID == request.auth.uid;
}
}
function existingData() {
return resource.data;
}
function incomingData() {
return request.resource.data;
}
}
existingData().authorID
とauthのUIDを比較する、つまり更新前のDBの状態でのauthorID
と比べる方法でも良いのではと思うかもしれませんが、これだと△です。
先程のようにしっかりと防ぐことができず、
- author本人が意図しても意図しなくても、別の
authorID
に書き換えができてしまう
といった問題が起きてしまいます。
書き換えを防いだ方が良いフィールド
- 作成時に書き込んだら変わらない他ドキュメントへの参照(IDやDocumentReference)
-
createTime
といった作成日の情報 - CloudFunctions等で書き込み、クライアント側からは更新をしない情報
これらは incomingData().xxx === existingData().xxx
といった書き方で、変更前後で書き換わっていないかどうかをしっかり見るようにすると良いでしょう。
まとめ
今回はちょっとした例を踏まえて、うっかりやってしまいそうな事例とその対処を紹介してみました。
自分の書いたルール、大丈夫か一度見直してみるといいかもしれません。