[Django REST Framework]CursorPaginationsのカーソルが無限ループする


TL;DR

Paginationクラスの、offset_cutoffNoneを設定する(ドキュメントに書いてないような…?)

環境

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)となる。
以降、無限ループに陥り、正しくデータが取得できない。
(対処法は、記事冒頭の通り)


問題は解決したのですが、もう少しだけソースコードを眺めてみました

# 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に制限されている