Repository: django-hijack/django-hijack (language: python)

Issue:
Performance degraded after upgrade to 3.4.1
Current setup

```
Django==3.2.21
django-hijack==2.1.10
```

In current setup, it takes 3-4 seconds when we submit the form and impersonate a user. After upgrading to `django-hijack==3.4.1`, time to impersonate has increased to 120 seconds.

If we remove LockUserTableMixin, it starts working correctly. We have a auth table with large number of entries and the below code is taking a long time to complete.

```
next(get_user_model()._base_manager.select_for_update().iterator())
```

I tested this in my local environment by adding large number of entries in auth table. The auth table was not being accessed or locked by any other process.

I see one another user which has faced the same issue and applying some temporary fix in forked repository (https://github.com/django-hijack/django-hijack/commit/4f4db9d379484941052aecf60ec082ad86703d45)






Performance degraded after upgrade to 3.4.1
Current setup

```
Django==3.2.21
django-hijack==2.1.10
```

In current setup, it takes 3-4 seconds when we submit the form and impersonate a user. After upgrading to `django-hijack==3.4.1`, time to impersonate has increased to 120 seconds.

If we remove LockUserTableMixin, it starts working correctly. We have a auth table with large number of entries and the below code is taking a long time to complete.

```
next(get_user_model()._base_manager.select_for_update().iterator())
```

I tested this in my local environment by adding large number of entries in auth table. The auth table was not being accessed or locked by any other process.

I see one another user which has faced the same issue and applying some temporary fix in forked repository (https://github.com/django-hijack/django-hijack/commit/4f4db9d379484941052aecf60ec082ad86703d45)








Discussion / hints:
Hi @vinodpandey,

Thanks for reaching out. That's a peculiar problem. Would you mind sharing which database you are using and which major version? Furthermore, can you specify what a large number is in your context? How many zeros are we talking about? As in: `10^x`

That aside, the lock is an important security measure, to avoid that concurrent requests injecting a false hijack history. A series of well time requests could be used to assume some's identity without any indication that the account was hijacked.

You could probably build a more performant lock using Redis. However, not everyone will use Redis in their Django app.

We could consider only locking a single row, not the entire user table. Can you test if that is faster for you? We would need to consider all possible attack vectors before doing such a change, but I'd be more than happy to help.

Cheers!
Joe
Hi @vinodpandey,

Thanks for reaching out. That's a peculiar problem. Would you mind sharing which database you are using and which major version? Furthermore, can you specify what a large number is in your context? How many zeros are we talking about? As in: `10^x`

That aside, the lock is an important security measure, to avoid that concurrent requests injecting a false hijack history. A series of well time requests could be used to assume some's identity without any indication that the account was hijacked.

You could probably build a more performant lock using Redis. However, not everyone will use Redis in their Django app.

We could consider only locking a single row, not the entire user table. Can you test if that is faster for you? We would need to consider all possible attack vectors before doing such a change, but I'd be more than happy to help.

Cheers!
Joe

Fix patch:
diff --git a/hijack/views.py b/hijack/views.py
index 8f5b8459..b3b23feb 100755
--- a/hijack/views.py
+++ b/hijack/views.py
@@ -63,8 +63,8 @@ def get_redirect_url(self):
 class LockUserTableMixin:
     @transaction.atomic()
     def dispatch(self, request, *args, **kwargs):
-        # Lock entire user table to avoid race conditions
-        next(get_user_model()._base_manager.select_for_update().iterator())
+        # Lock user row to avoid race conditions
+        get_user_model()._base_manager.select_for_update().get(pk=request.user.pk)
         return super().dispatch(request, *args, **kwargs)
 
 


Test patch:
diff --git a/hijack/tests/test_views.py b/hijack/tests/test_views.py
index a44e8906..cf3a0553 100644
--- a/hijack/tests/test_views.py
+++ b/hijack/tests/test_views.py
@@ -18,7 +18,9 @@ def test_dispatch__custom_manager(self, admin_user, rf):
         class LockedView(views.LockUserTableMixin, View):
             pass
 
-        LockedView().dispatch(rf.get("/"))
+        request = rf.get("/")
+        request.user = admin_user
+        LockedView().dispatch(request)
 
 
 class TestSuccessUrlMixin: