TL;DR
Paginationクラスの、offset_cutoff
にNone
を設定する(ドキュメントに書いてないような…?)
- offset_cutoffの説明
- offset_cutoff=Noneの場合、offsetは制限されない
- プルリク
環境
django-rest-framework 3.9.4
現象
以下のCursorPaginationクラスを考える
class CursorPagination(pagination.CursorPagination):
page_size_query_param = 'page_size'
page_size = 100
ordering = '-last_modified'
ViewSetのpagination_classにCursorPaginationを使ったViewからは、以下のようなレスポンスが返る。
{
'next': 'http://hogehoge.com/api/?cursor=bz0xMTAwJnA9MjAwNS0wNy0yNCsxNiUzQTAxJTNBMTElMkIwMCUzQTAw',
'data': ...
}
?cursor=
以降は、データの部分集合を特定するための文字列(base64)。
next
のURLへのアクセスを繰り返すことで、すべてのデータを取得することが出来る。
しかし、last_modified
が同じデータを1000件程取得すると、(リクエストURL)=(nextのURL)となる。
以降、無限ループに陥り、正しくデータが取得できない。
(対処法は、記事冒頭の通り)
問題は解決したのですが、もう少しだけソースコードを眺めてみました
- DRFのソースコード( https://github.com/encode/django-rest-framework/blob/3.9.4/rest_framework/pagination.py )を読みました
# The offset in the cursor is used in situations where we have a
# nearly-unique index. (Eg millisecond precision creation timestamps)
# We guard against malicious users attempting to cause expensive database
# queries, by having a hard cap on the maximum possible size of the offset.
-
OFFSET操作は重いので、それを悪用した攻撃を防ぐための措置?
-
cursorのbase64文字列をデコード→URLデコードすると、
o=1100&p=2005-07-24+16:01:11+00:00
のようになる- oはオフセット、pは位置を示す(参考)
- pは
ordering
に指定された属性を参照し、値が決まる- デフォルト値は
created
、上記の例ではlast_modified
- デフォルト値は
- pの値が
nearly-unique
であり、かつmax_page_num
個以上あるときは、取得できたデータの個数分オフセットを進めたcursorが生成される
-
つまり、攻撃者は、o(オフセット)を過剰に大きく設定したcursorを作成して、リクエストを投げる?
- 正常にデータが取得できるとき、expensive database queriesによる攻撃(DDos?)が成立する?
- ので、対策としてオフセットの上限が1000に制限されている