mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-04 14:10:52 +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:
@@ -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())
|
||||||
|
|||||||
@@ -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'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user