From 2ca7caf73fe1aa1c74102e363d77f4983c57f6a4 Mon Sep 17 00:00:00 2001 From: Lewis Collard Date: Wed, 1 Jul 2020 21:20:12 +0100 Subject: [PATCH 1/2] Don't allow anonymous users to upload files by poking the /markdownx/upload endpoint directly. --- dev.xml | 14 ++++++++++++++ docs-src/customization.md | 13 +++++++++++++ markdownx/settings.py | 2 ++ markdownx/tests/tests.py | 39 ++++++++++++++++++++++++++++++++++++--- markdownx/views.py | 19 +++++++++++++++++-- 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/dev.xml b/dev.xml index 9183451..83db259 100644 --- a/dev.xml +++ b/dev.xml @@ -151,6 +151,13 @@ configure_settings = { }, ], 'ROOT_URLCONF': 'tests.urls', + 'MIDDLEWARE': [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ], } settings.configure(**configure_settings) @@ -289,6 +296,13 @@ configure_settings = { }, ], 'ROOT_URLCONF': 'tests.urls', + 'MIDDLEWARE': [ + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ], } settings.configure(**configure_settings) diff --git a/docs-src/customization.md b/docs-src/customization.md index 4b90ceb..2cb7435 100644 --- a/docs-src/customization.md +++ b/docs-src/customization.md @@ -63,6 +63,7 @@ You may place any of the variables outlined in this page in your `settings.py`, * [`MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS`](#markdownx_markdown_extension_configs) * [`MARKDOWNX_URLS_PATH`](#markdownx_urls_path) * [`MARKDOWNX_UPLOAD_URLS_PATH`](#markdownx_upload_urls_path) +* [`MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS`](#markdownx_upload_allow_anonymous) * [`MARKDOWNX_MEDIA_PATH`](#markdownx_media_path) * [`MARKDOWNX_UPLOAD_MAX_SIZE`](#markdownx_upload_max_size) * [`MARKDOWNX_UPLOAD_CONTENT_TYPES`](#markdownx_upload_content_types) @@ -152,6 +153,18 @@ Relative URL to which the Markdown text is sent to be encoded as HTML. MARKDOWNX_URLS_PATH = '/markdownx/markdownify/' ``` + +### `MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS` + +Default: `False` + +Set to `True` if you wish to allow image uploads from anonymous (unauthenticated) users. If you are using MarkdownX in your admin exclusively, or otherwise only to users who are authenticated, you almost certainly do not want to change this from the default of `False`. + +```python +MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS = False +``` + + ### `MARKDOWNX_UPLOAD_URLS_PATH` Default: `'/markdownx/upload/'` diff --git a/markdownx/settings.py b/markdownx/settings.py index d9be7c1..fd59223 100755 --- a/markdownx/settings.py +++ b/markdownx/settings.py @@ -59,6 +59,8 @@ def _mdx(var, default): # Image # -------------------- +MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS = _mdx('UPLOAD_ALLOW_ANONYMOUS', False) + MARKDOWNX_UPLOAD_MAX_SIZE = _mdx('UPLOAD_MAX_SIZE', FIFTY_MEGABYTES) MARKDOWNX_UPLOAD_CONTENT_TYPES = _mdx('UPLOAD_CONTENT_TYPES', VALID_CONTENT_TYPES) diff --git a/markdownx/tests/tests.py b/markdownx/tests/tests.py index 1dc811f..6d621fe 100644 --- a/markdownx/tests/tests.py +++ b/markdownx/tests/tests.py @@ -1,18 +1,51 @@ import os import re +from contextlib import contextmanager +from unittest import mock + from django.test import TestCase from django.urls import reverse class SimpleTest(TestCase): - def test_upload(self): + @contextmanager + def _get_image_fp(self): + full_path = os.path.join( + os.path.dirname(__file__), + 'static', + 'django-markdownx-preview.png', + ) + with open(full_path, 'rb') as fp: + yield fp + + def test_upload_anonymous_fails(self): url = reverse('markdownx_upload') - with open('markdownx/tests/static/django-markdownx-preview.png', 'rb') as fp: + + # Test that image upload fails for an anonymous user when + # MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS is the default False. + with self._get_image_fp() as fp: response = self.client.post(url, {'image': fp}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - json = response.json() + self.assertEqual(response.status_code, 403) + + def test_upload_anonymous_succeeds_with_setting(self): + """ + Ensures that uploads succeed when MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS + is True. This implicitly tests the authenticated case as well. + """ + url = reverse('markdownx_upload') + + # A patch is required here because the view sets the + # MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS at first import, reading from + # django.conf.settings once, which means Django's standard + # override_settings helper does not work. There's probably a case for + # re-working the app-local settings. + with mock.patch('markdownx.settings.MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS', True): + with self._get_image_fp() as fp: + response = self.client.post(url, {'image': fp}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) + json = response.json() self.assertIn('image_code', json) match = re.findall(r'(markdownx/[\w\-]+\.png)', json['image_code']) diff --git a/markdownx/views.py b/markdownx/views.py index 2a72aac..e9e9bd2 100755 --- a/markdownx/views.py +++ b/markdownx/views.py @@ -1,13 +1,14 @@ +from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.http import JsonResponse from django.utils.module_loading import import_string from django.views.generic.edit import BaseFormView from django.views.generic.edit import View +from . import settings from .forms import ImageForm -from .settings import MARKDOWNX_MARKDOWNIFY_FUNCTION -markdownify_func = import_string(MARKDOWNX_MARKDOWNIFY_FUNCTION) +markdownify_func = import_string(settings.MARKDOWNX_MARKDOWNIFY_FUNCTION) class MarkdownifyView(View): @@ -39,6 +40,20 @@ class ImageUploadView(BaseFormView): form_class = ImageForm success_url = '/' + def dispatch(self, request, *args, **kwargs): + """ + Raises PermissionDenied if the current user is not authenticated and + MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS is not set. + + :param request: Django request + :type request: django.http.request.HttpRequest + :rtype: django.http.JsonResponse, django.http.HttpResponse + """ + if not settings.MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS and not request.user.is_authenticated: + raise PermissionDenied + + return super().dispatch(request, *args, **kwargs) + def form_invalid(self, form): """ Handling of invalid form events. From 91d5349b2458e908bd4d4e138564707517fbd9c6 Mon Sep 17 00:00:00 2001 From: Lewis Collard Date: Thu, 9 Nov 2023 01:33:39 +0000 Subject: [PATCH 2/2] Document that `request.user` must be available. --- docs-src/installation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs-src/installation.md b/docs-src/installation.md index 939c9fd..4caba49 100644 --- a/docs-src/installation.md +++ b/docs-src/installation.md @@ -8,6 +8,11 @@ Django MarkdownX may be installed directly using Python Package Index (PyPi): python3 -m pip install django-markdownx ``` +`request.user` must be available via some middleware if you wish to restrict +uploads to logged-in users via the `MARKDOWNX_UPLOAD_ALLOW_ANONYMOUS` setting. +Most likely, you are using `django.contrib.auth.middleware.AuthenticationMiddleware` +already, which sets this attribute. + ## From the source Should you wish to download and install it using the source code, you can do as follows: