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."""
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:
"""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
# Additional CSRF settings
CSRF_FAILURE_VIEW = 'InvenTree.middleware.csrf_failure'
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_COOKIE_NAME = 'csrftoken'
@@ -2,8 +2,7 @@
from unittest.mock import patch
from django.conf import settings
from django.http import Http404
from django.http import Http404, HttpRequest
from django.urls import reverse
from error_report.models import Error
@@ -289,18 +288,63 @@ class MiddlewareTests(InvenTreeTestCase):
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
self.assertContains(
response,
'INVE-E7: The visited path `http://testserver` does not match',
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:
notifications.show({
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',
color: 'red'
});