Opened 7 years ago

Closed 5 years ago

#28699 closed Bug (fixed)

REMOTE_USER issues with CSRF

Reported by: stephanm Owned by: Colton Hicks
Component: contrib.auth Version: 1.11
Severity: Normal Keywords: remote user
Cc: Florian Apolloner, Carlton Gibson Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I have a problem with csrf protection starting with django 1.11.6
(django 1.11.5 has not this problem).

I am doing all time exactly what is explained in
https://docs.djangoproject.com/en/1.11/howto/auth-remote-user/

My settings:

     MIDDLEWARE = [
        "django.contrib.sessions.middleware.SessionMiddleware",
        "django.middleware.locale.LocaleMiddleware",
        "django.middleware.common.CommonMiddleware",
        "django.middleware.csrf.CsrfViewMiddleware",
        "django.contrib.auth.middleware.AuthenticationMiddleware",
        # "django.contrib.auth.middleware.RemoteUserMiddleware",
        # own middleware because behind proxy we get HTTP_REMOTE_USER 
        # instead of REMOTE_USER
        "lib.auth.middleware.RemoteUserMiddlewareProxy",
        "django.contrib.messages.middleware.MessageMiddleware",
        "django.middleware.clickjacking.XFrameOptionsMiddleware",
    ]
    
    AUTHENTICATION_BACKENDS = [
        # "django.contrib.auth.backends.RemoteUserBackend",
        "lib.auth.backends.RemoteUserBackendTooling",
        # default is:
        "django.contrib.auth.backends.ModelBackend",
    ]
# content of lib.auth.middleware.RemoteUserMiddlewareProxy
from django.contrib.auth.middleware import RemoteUserMiddleware


class RemoteUserMiddlewareProxy(RemoteUserMiddleware):
    header = "HTTP_REMOTE_USER"
# content of lib.auth.backends.RemoteUserBackendTooling
from django.contrib.auth.backends import RemoteUserBackend


class RemoteUserBackendTooling(RemoteUserBackend):

    create_unknown_user = False

    def clean_username(self, username):
        """
        Performs any cleaning on the "username" prior to using it to get or
        create the user object.  Returns the cleaned username.

        By default, returns the username unchanged.
        """
        if username.startswith("IT\\"):
            username = username[3:]
        return username
"

My C# Application does a login by using the normal
"django.contrib.auth.backends.ModelBackend", not using the REMOTE_USER !
It calls a function in my views.py:

    def auth_login_json(request):
        # code...
        # POST data with user & password

Now the csrf protection fails with an http error 403
(only with django 1.11.6, ... django 1.11.5 works)

I found two possibilities to make it work again:

  1. In MIDDLEWARE, comment out "lib.auth.middleware.RemoteUserMiddlewareProxy" but the other remote user login functionality is gone.
  2. I add @csrf_exempt to auth_login_json function like this:
    @csrf_exempt
    def auth_login_json(request):
        # code...
        # POST data with user & password

Reading the changelog https://docs.djangoproject.com/en/1.11/releases/1.11.6/
I suppose this behaviour change comes with https://code.djangoproject.com/ticket/28488

My question: Was I wrong all the years or is this a bug?

Change History (30)

comment:1 by Tim Graham, 7 years ago

Cc: Florian Apolloner added
Component: UncategorizedCSRF
Type: UncategorizedBug

comment:2 by Florian Apolloner, 7 years ago

Can your share your code/setup? I do not see anything obvious -- your C# app should always have gotten an CSRF error, or did it include a csrf token?

comment:3 by Florian Apolloner, 7 years ago

Actually I might have an idea, can you check if commenting out https://github.com/django/django/blob/4d60261b2a77460b4c127c3d832518b95e11a0ac/django/contrib/auth/__init__.py#L128 fixes the issue? This seems to be caused by the auth.login call from the RemoteUserMiddleware which then resets tokens :/

comment:4 by stephanm, 7 years ago

Hi Florian,

just commented out the "rotate_token(request)" line in login as you told me.

Now it works again.

Perhaps I am doing something wrong too, I didn't understand exactly the
csrf workflow.
I use Apache on Windows with a plugin which allows me to use NTLM as Single Sign On.
My django runs as reverse proxy and gets the remote_user from apache,
which is intended for the normal users which come with their browsers.

But my c# application does a normal login.

Is there some howto explainig how an external program c#
should login, showing when and how the csrf tokens
appears in the cookies during the HTTP conversation
and what of them should be taken?

Thanks.

comment:5 by Florian Apolloner, 7 years ago

Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted

Ok, thanks -- given this I can reproduce it. This is a bug in Django (kinda), but probably a hard one to fix :(

comment:6 by Florian Apolloner, 7 years ago

Actually I am still not sure what and why is happening here. How does your C# app login exactly? Ie where from does it get the csrf token and is the C# app affected by the single sign on stuff?

comment:7 by Florian Apolloner, 7 years ago

Please restore the original Django 1.11.6 and move

       "django.contrib.auth.middleware.AuthenticationMiddleware",
        "lib.auth.middleware.RemoteUserMiddlewareProxy",

before the CSRF middleware. The issue should be gone then, which will probably mean that the fix will just be a documentation fix.

Last edited 7 years ago by Florian Apolloner (previous) (diff)

comment:8 by stephanm, 7 years ago

Hi,

I restored the original Django 1.11.6 and moved the lines you mentioned
*before* before the CSRF middleware and I can confirm that it works now!

Concerning my C# Application, It calls the following
function in my views.py with a GET call to
get the carf_token:

def auth_get_csrf_token_json(request):
    token = csrf(request)
    csrf_token = str(token["csrf_token"])  # ab django 1.5
    response = JsonResponse({"dataType": "csrf", "data": {"csrftoken": csrf_token}})
    # I set the cookie in the past but it seems not necessary
    ##response.set_cookie("csrftoken", csrf_token)
    return response

Note:

  • I send back the csrf token in as json data but in my C# app I use the csrf token which is in the cookie.
  • One strange thing I didn't understand: the csrf token in the returned json data and in the cookie are different

Honestly I was never sure where to get this initial csrf token
to be able to POST my login data.
So I did my experiments until I found this solution which worked for me
(some times ago).

comment:9 by Florian Apolloner, 7 years ago

One strange thing I didn't understand: the csrf token in the returned json data and in the cookie are different

Yes, the token changes every request to account for BREACH style attacks. you have to take the first half of it and xor it to the second one (basically) to get the constant "secret" behind it which is reused during the requests.

As for your code:

from django.middleware.csrf import get_token
get_token(request)

in your view should be enough, Django will take care of setting the cookie etc accordingly.

in reply to:  7 comment:10 by stephanm, 7 years ago

Replying to Florian Apolloner:

...
before the CSRF middleware. The issue should be gone then, which will probably mean that the fix will just be a documentation fix.

If the documentation fix is about to place AuthenticationMiddleware
before the CsrfViewMiddleware in the MIDDLEWARE setting then you
will have to do more than only changing the docs:

  • You will have to change: django-admin startproject so that it generates the appropriate middleware ordering.
  • You will have to tell everybody that their settings.MIDDLEWARE has to be modified, otherwise some functionality may be broken
  • modify perhaps some other places in the docs i missed ...

Is it really only a documentation fix?

comment:11 by Florian Apolloner, 7 years ago

It only affects the RemoteUserMiddleware, which is not enabled by default.

comment:12 by stephanm, 7 years ago

Aha ... so, the fix will be whats mentioned in comment:7,
the move of django.contrib.auth.middleware.AuthenticationMiddleware
and the RemoteUserMiddleware... ? (plus fixes in the docs of course)

Right? Or do you plan to do other changes?

I ask this, so I could fix my code now.

comment:13 by Florian Apolloner, 7 years ago

Yes, something along those lines will be the final fix. I need to think about it a bit more though, cannot gurantee if or what I missed.

comment:14 by Tim Graham, 7 years ago

Component: CSRFDocumentation
Severity: Release blockerNormal
Summary: Problem with CSRF in Django 1.11.6Document middleware ordering requirements following CSRF change in Django 1.11.6

comment:15 by Rodrigo, 6 years ago

Has patch: set
Owner: changed from nobody to Rodrigo
Status: newassigned

comment:16 by Carlton Gibson, 6 years ago

The PR here looks fine, re the discussion, but I'm missing a crucial part in my understanding here: I can't see how you're meant to make a successful CSRF check in the same request as a REMOTE_USER login...

As I read it, if RemoteUserMiddleware is first then:

  • RemoteUserMiddleware.process_request() calls auth.login() which calls rotate_token():
def rotate_token(request):
    """
    Change the CSRF token in use for a request - should be done on login
    for security purposes.
    """
    request.META.update({
        "CSRF_COOKIE_USED": True,
        "CSRF_COOKIE": _get_new_csrf_token(),
    })
    request.csrf_cookie_needs_reset = True
  • But then CsrfViewMiddleware.process_request() calls _get_token() which fetches the old CSRF_COOKIE, from either the session or the cookie, and resets it on `request.META`.

Unless I missed something, this is negating the rotate_token() call. Is that correct, or have I misread it? Given the docstring in rotate_token() isn't this a no-no?

On the other hand, if CsrfViewMiddleware is first then the rotate_token() call will replace whatever CSRF_COOKIE was previously set, and so the actual CSRF check in process_view() will necessarily fail. (As we must be seeing here.)

Version 0, edited 6 years ago by Carlton Gibson (next)

comment:17 by Carlton Gibson, 6 years ago

Cc: Carlton Gibson added

in reply to:  16 comment:18 by Florian Apolloner, 6 years ago

Replying to Carlton Gibson:

Unless I missed something, this is negating the rotate_token() call. Is that correct, or have I misread it? Given the docstring in rotate_token() isn't this a no-no? If so, we can't recommend this.

Yes, this seems correct

On the other hand, if CsrfViewMiddleware is first then the rotate_token() call will replace whatever CSRF_COOKIE was previously set, and so the actual CSRF check in process_view() will necessarily fail. (As we must be seeing here.)

Also correct. I wonder if my patch in #28488 actually made the situation worse (speaks for the complexity of the middleware :/)

comment:19 by Carlton Gibson, 6 years ago

Patch needs improvement: set

Okay. Thank you for following up Florian. I will dig dipper given your confirmation of my first reading.

comment:20 by Carlton Gibson, 5 years ago

Right, some time later...

I think that prior to c4c128d67c7dc2830631c6859a204c9d259f1fb1 (for #28488) there was actually a security issue in the way CsrfViewMiddleware worked.
It would call _get_token() in process_view(), resetting the CSRF token to the one submitted in the request, even though rotate_token() had previously been
called during login() by the RemoteUserMiddleware.

This is equivalent to the CsrfViewMiddleware second case discussed above. It allows the Login + CSRF check in the single request but is not safe.
As such, that the behaviour changed slightly cannot be considered a regression. (It should never have worked.)

Possibly RemoteUserMiddleware could be adjusted to defer rotating the CSRF token (until say process_response()), but anything in that ball-park is highly sensitive, and probably not worth the price of admission.

For this ticket I think documenting that remote user auth will require two requests — one to login, on to submit further data passing CSRF — is the best we can do.

comment:21 by Carlton Gibson, 5 years ago

Has patch: unset
Keywords: remote user added
Patch needs improvement: unset
Summary: Document middleware ordering requirements following CSRF change in Django 1.11.6Document that REMOTE_USER must be logged in before making CSRF protected requests.

comment:22 by Carlton Gibson, 5 years ago

For this ticket I think documenting that remote user auth will require two requests — one to login, on to submit further data passing CSRF — is the best we can do.

Actually, I'm not exactly sure what to say here. Thinking about it, exactly the same considerations apply to all login. You'd have to take special measures to login a user and submit additional form data, whilst also checking CSRF, in a single request, even if you were using session based authentication with the model backend. (You'd write a view to do it, manually calling login() yourself...)

I'm kind of inclined towards wontfix for that reason...

in reply to:  22 comment:23 by Rodrigo, 5 years ago

Replying to Carlton Gibson:

For this ticket I think documenting that remote user auth will require two requests — one to login, on to submit further data passing CSRF — is the best we can do.

Actually, I'm not exactly sure what to say here. Thinking about it, exactly the same considerations apply to all login. You'd have to take special measures to login a user and submit additional form data, whilst also checking CSRF, in a single request, even if you were using session based authentication with the model backend. (You'd write a view to do it, manually calling login() yourself...)

I'm kind of inclined towards wontfix for that reason...

I am not sure if I am understanding, there will be always one extra request for getting the CSRF token before POSTing anything. The rotate_token() is called only by login(), the RemoteUserMiddleware calls it only when user is not auth'ed (otherwise it just return) - which as you said before, it will be negated later by CSRF.process_view().

If this would have worked as expected, the documentation should say something like "After a login, the client needs to update the CSRF one more time to keep posting in further requests - you may return it in your login view to save one request", which applies to all logins - as you said.

In this case, as it is overridden, there is no need, because the view would be executed anyway and the data procesed - though without the rotate_token() protection.

A fix for this that comes into my mind would be a "hook" on login to deffer the token rotation and set a "user_has_logged" flag if REMOTE_USER is enabled and then catch it later - as you said - to trigger the rotation. This way it would behave as other backends.

Otherwise, it should be documented that token rotation on login is not functioning for the REMOTE_USER backend, so beware! :)

comment:24 by Mariusz Felisiak <felisiak.mariusz@…>, 5 years ago

In 94469504:

Refs #28699 -- Clarified CSRF middleware ordering in relation to RemoteUserMiddleware.

comment:25 by Mariusz Felisiak <felisiak.mariusz@…>, 5 years ago

In b0b98fca:

[3.0.x] Refs #28699 -- Clarified CSRF middleware ordering in relation to RemoteUserMiddleware.

Backport of 94469504706b494877b6bb45a979bcb81c7fd7be from master

comment:26 by Mariusz Felisiak <felisiak.mariusz@…>, 5 years ago

In 09013aa:

[2.2.x] Refs #28699 -- Clarified CSRF middleware ordering in relation to RemoteUserMiddleware.

Backport of 94469504706b494877b6bb45a979bcb81c7fd7be from master

comment:27 by Rodrigo, 5 years ago

Component: Documentationcontrib.auth
Has patch: set
Summary: Document that REMOTE_USER must be logged in before making CSRF protected requests.REMOTE_USER issues with CSRF

I think I got it! :)

First I implemented the deferred token rotation as Carlton suggested. I introduced a backend setting called "CSRF_DEFER_TOKEN_ROTATION" to the REMOTE_USER backend, then modified the login function to check if the backend has that option do not rotate the token and defer it, then catch it on CSRF's process_response, not REMOTE_USER's.

It worked. But... I then realized that every backend should have that option because it's CSRF MW responsability to do that, not the login function, which lead me to realize that it's kind of a break of the separation of concerns, the login function as stands before is tied to the CSRF MW and if CSRF MW is not enabled, the django login would not work, making it not loosely coupled with the MW.

Then, the solution is to delegate entirely the token rotation to the CSRF MW and do it at the end of the request-response cycle, so if there is a valid token, process the request and then rotate the token for the next request.

After trying different options, like aCSRF_TOKEN_ROTATION_ON_LOGIN general setting, the best and minimal solution I found is just reuse the previous defined vars, add another flag for rotation - csrf_token_rotation - and just delegate it to the MW if the MW is enabled.

By doing this, token rotation for the REMOTE_USER backend should be fixed without the need of reordering.

I removed the CSRF_TOKEN_ROTATION_ON_LOGIN setting to disable the feature, but as the rotation works as expected, it has a very limited application (only troubleshooting comes to my mind) though I can re-submit it later if you consider it.

PR

comment:28 by Carlton Gibson, 5 years ago

Needs tests: set

New PR that suggests using CsrfViewMiddleware._get_token() in process_view(). (This was how the token was fetched prior to c4c128d67c7dc2830631c6859a204c9d259f1fb1.) If there's no issue with that, then it looks feasible.

Needs tests.

comment:29 by Carlton Gibson, 5 years ago

Owner: changed from Rodrigo to Colton Hicks
Triage Stage: AcceptedReady for checkin

comment:30 by Carlton Gibson <carlton@…>, 5 years ago

Resolution: fixed
Status: assignedclosed

In f283ffa:

Fixed #28699 -- Fixed CSRF validation with remote user middleware.

Ensured process_view() always accesses the CSRF token from the session
or cookie, rather than the request, as rotate_token() may have been called
by an authentication middleware during the process_request() phase.

Note: See TracTickets for help on using tickets.
Back to Top