2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-07-04 06:00:38 +00:00

Add meaningful message on CSRF failure (#12216)

* Add meaningful message on CSRF failure

* Add link to CSRF_FAILURE_VIEW

* Add unit test for new CSRF feedback
This commit is contained in:
Oliver
2026-06-20 23:49:36 +10:00
committed by GitHub
parent 8afc8b3d50
commit 50577da65a
4 changed files with 85 additions and 12 deletions
@@ -106,6 +106,32 @@ apps_mfa_bypass = [
"""App namespaces that bypass MFA enforcement - normal security model is still enforced.""" """App namespaces that bypass MFA enforcement - normal security model is still enforced."""
def csrf_failure(request, reason=''):
"""Custom CSRF failure handler.
Returns a JSON response for API/headless requests so the frontend can
provide a meaningful error message to the user
"""
from django.views.csrf import csrf_failure as django_default
if (
request.path.startswith('/_allauth/')
or request.path.startswith('/api/')
or 'application/json' in request.headers.get('Accept', '')
or 'application/json' in request.headers.get('Content-Type', '')
):
return JsonResponse(
{
'detail': _(
'CSRF verification failed. Ensure INVENTREE_SITE_URL and INVENTREE_TRUSTED_ORIGINS are configured correctly.'
)
},
status=403,
)
return django_default(request, reason=reason)
class AuthRequiredMiddleware: class AuthRequiredMiddleware:
"""Check for user to be authenticated.""" """Check for user to be authenticated."""
@@ -851,6 +851,7 @@ valid_cookie_modes = ['lax', 'strict', 'none']
COOKIE_MODE = COOKIE_MODE.capitalize() if COOKIE_MODE in valid_cookie_modes else False COOKIE_MODE = COOKIE_MODE.capitalize() if COOKIE_MODE in valid_cookie_modes else False
# Additional CSRF settings # Additional CSRF settings
CSRF_FAILURE_VIEW = 'InvenTree.middleware.csrf_failure'
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_COOKIE_NAME = 'csrftoken' CSRF_COOKIE_NAME = 'csrftoken'
@@ -2,8 +2,7 @@
from unittest.mock import patch from unittest.mock import patch
from django.conf import settings from django.http import Http404, HttpRequest
from django.http import Http404
from django.urls import reverse from django.urls import reverse
from error_report.models import Error from error_report.models import Error
@@ -289,18 +288,63 @@ class MiddlewareTests(InvenTreeTestCase):
response, 'window.INVENTREE_SETTINGS', status_code=500 response, 'window.INVENTREE_SETTINGS', status_code=500
) )
# Log stuff # TODO remove
print(
'###DBG-TST###',
'site',
settings.SITE_URL,
'trusted',
settings.CSRF_TRUSTED_ORIGINS,
)
# Check that the correct step triggers the error message # Check that the correct step triggers the error message
self.assertContains( self.assertContains(
response, response,
'INVE-E7: The visited path `http://testserver` does not match', 'INVE-E7: The visited path `http://testserver` does not match',
status_code=500, status_code=500,
) )
def test_csrf_failure(self):
"""Test the custom CSRF failure handler."""
from InvenTree.middleware import csrf_failure
EXPECTED_DETAIL = 'CSRF verification failed. Ensure INVENTREE_SITE_URL and INVENTREE_TRUSTED_ORIGINS are configured correctly.'
def make_request(path, headers=None):
request = HttpRequest()
request.path = path
request.META['SERVER_NAME'] = 'testserver'
request.META['SERVER_PORT'] = '80'
for key, value in (headers or {}).items():
request.META[f'HTTP_{key.upper().replace("-", "_")}'] = value
return request
# API path -> JSON 403 with meaningful message
response = csrf_failure(
make_request('/api/part/'), reason='origin check failed'
)
self.assertEqual(response.status_code, 403)
import json
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# allauth headless path -> JSON 403
response = csrf_failure(make_request('/_allauth/browser/v1/auth/login'))
self.assertEqual(response.status_code, 403)
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# Accept: application/json header -> JSON 403
response = csrf_failure(
make_request('/some/other/path/', {'Accept': 'application/json'}),
reason='origin check failed',
)
self.assertEqual(response.status_code, 403)
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# Content-Type: application/json header -> JSON 403
response = csrf_failure(
make_request('/some/other/path/', {'Content-Type': 'application/json'}),
reason='origin check failed',
)
self.assertEqual(response.status_code, 403)
data = json.loads(response.content)
self.assertEqual(data['detail'], EXPECTED_DETAIL)
# Plain browser request -> falls back to Django default CSRF page (403 HTML, not JSON)
response = csrf_failure(make_request('/web/'), reason='origin check failed')
self.assertEqual(response.status_code, 403)
self.assertNotIn(b'application/json', response.get('Content-Type', '').encode())
+3 -1
View File
@@ -131,7 +131,9 @@ export async function doBasicLogin(
default: default:
notifications.show({ notifications.show({
title: `${t`Login failed`} (${err.response.status})`, title: `${t`Login failed`} (${err.response.status})`,
message: t`Check your input and try again.`, message:
err.response?.data?.detail ??
t`Check your input and try again.`,
id: 'auth-login-error', id: 'auth-login-error',
color: 'red' color: 'red'
}); });