diff --git a/docs/roadmap.md b/docs/roadmap.md index 1022c2ab9e2a1..9bbe7ca4c0d4d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -179,9 +179,9 @@ of its size, it takes work to keep it that way. * [Add support for 2-factor authentication on all platforms](https://github.com/zulip/zulip/pull/1747) -* [Add support for stronger security controls for uploaded files (The +* [Add support for stronger security controls for uploaded files (The LOCAL_UPLOADS_DIR file uploads backend only supports world-readable - uploads)](https://github.com/zulip/zulip/issues/320) + uploads)](https://github.com/zulip/zulip/issues/320) * [Fix requirement to set a password when creating account via Google](https://github.com/zulip/zulip/issues/1633) * [Add a retention policy feature that automatically deletes old diff --git a/docs/security-model.md b/docs/security-model.md index 0fe3fcd5e8ff3..d742dcc21ddce 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -184,24 +184,9 @@ your organization. * Zulip supports user-uploaded files; ideally they should be hosted from a separate domain from the main Zulip server to protect against various same-domain attacks (e.g. zulip-user-content.example.com) - using the S3 integration. - - The URLs of user-uploaded files are secret; if you are using the - "local file upload" integration, anyone with the URL of an uploaded - file can access the file. This means the local uploads integration - is vulnerable to a subtle attack where if a user clicks on a link in - a secret .PDF or .HTML file that had been uploaded to Zulip, access - to the file might be leaked to the other server via the Referrer - header (see [the "Uploads world readable" issue on - GitHub](https://github.com/zulip/zulip/issues/320)). - - The Zulip S3 file upload integration is relatively safe against that - attack, because the URLs of files presented to users don't host the - content. Instead, the S3 integration checks the user has a valid - Zulip session in the relevant realm, and if so then redirects the - browser to a one-time S3 URL that expires a short time later. - Keeping the URL secret is still important to avoid other users in - the Zulip realm from being able to access the file. + using the S3 integration. The uploaded files could be viewed by only + those users who have access to them. Simple possession of a URL to + the uploaded file doesn't qualify as a right to view such a file. * Zulip supports using the Camo image proxy to proxy content like inline image previews that can be inserted into the Zulip message diff --git a/puppet/zulip/files/nginx/sites-available/zulip-enterprise b/puppet/zulip/files/nginx/sites-available/zulip-enterprise index b2f3e3a0ecf40..54945171b1890 100644 --- a/puppet/zulip/files/nginx/sites-available/zulip-enterprise +++ b/puppet/zulip/files/nginx/sites-available/zulip-enterprise @@ -12,7 +12,8 @@ server { ssl_certificate /etc/ssl/certs/zulip.combined-chain.crt; ssl_certificate_key /etc/ssl/private/zulip.key; - location /user_uploads { + location /serve_uploads { + internal; add_header X-Content-Type-Options nosniff; include /etc/nginx/zulip-include/uploads.types; alias /home/zulip/uploads/files; diff --git a/requirements/common.txt b/requirements/common.txt index 57b831d13d987..c7ef0d774438c 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -186,3 +186,8 @@ ijson==2.3 # Needed for link preview beautifulsoup4==4.5.3 git+https://github.com/rafaelmartins/pyoembed.git@eb9901917c2a44b49e2887c077ead84a722c50dc#egg=pyoembed + +# Needed for serving uploaded files from nginx but perform auth checks in django. +# We are using a commit from a PR https://github.com/johnsensible/django-sendfile/pull/64 +# in Django-sendfile repo as it might be sometime before it gets merged. +git+https://github.com/adnrs96/django-sendfile.git@f45fac5fdd83e7d42a8d5275038e33f8c2cdb920#egg=django-sendfile==0.3.12 diff --git a/version.py b/version.py index 9099a73eb0c3c..34f319cbf9c30 100644 --- a/version.py +++ b/version.py @@ -1,2 +1,2 @@ ZULIP_VERSION = "1.5.1+git" -PROVISION_VERSION = '5.0' +PROVISION_VERSION = '5.01' diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index bcc24b8915d15..0ca248b45bb8f 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -24,7 +24,7 @@ from zerver.lib.actions import do_delete_old_unclaimed_attachments from zilencer.models import Deployment -from zerver.views.upload import upload_file_backend +from zerver.views.upload import upload_file_backend, serve_local import ujson from six.moves import urllib @@ -43,6 +43,8 @@ import base64 from datetime import timedelta from django.utils.timezone import now as timezone_now +from django.http import HttpRequest +from sendfile import _get_sendfile from moto import mock_s3 @@ -580,6 +582,21 @@ def test_file_download_authorization_public(self): self.assertEqual(b"zulip!", data) self.logout() + def test_serve_local(self): + # type: () -> None + with self.settings(SENDFILE_BACKEND='sendfile.backends.nginx', + NGINX_VERSION='1.4.6'): + _get_sendfile.clear() # To clearout cached version of backend from djangosendfile + filename = os.path.join(settings.LOCAL_UPLOADS_DIR, 'files', 'á.jpg') + basedir = os.path.dirname(filename) + if not os.path.exists(basedir): + os.makedirs(basedir) + open(filename, 'w').close() + responce = serve_local(HttpRequest(), 'á.jpg') + _get_sendfile.clear() + test_upload_dir = os.path.split(settings.LOCAL_UPLOADS_DIR)[1] + self.assertEqual(responce['X-Accel-Redirect'], '/serve_uploads/../../' + test_upload_dir + '/files/á.jpg') + def tearDown(self): # type: () -> None destroy_uploads() diff --git a/zerver/views/upload.py b/zerver/views/upload.py index 0a5f14f1d7c51..eb38ec66418b3 100644 --- a/zerver/views/upload.py +++ b/zerver/views/upload.py @@ -14,24 +14,19 @@ from zerver.lib.validator import check_bool from zerver.models import UserProfile, validate_attachment_request from django.conf import settings +from sendfile import sendfile def serve_s3(request, url_path): # type: (HttpRequest, str) -> HttpResponse uri = get_signed_upload_url(url_path) return redirect(uri) -# TODO: Rewrite this once we have django-sendfile def serve_local(request, path_id): # type: (HttpRequest, str) -> HttpResponse - import os - import mimetypes local_path = get_local_file_path(path_id) if local_path is None: return HttpResponseNotFound('

File not found

') - filename = os.path.basename(local_path) - response = FileResponse(open(local_path, 'rb'), - content_type = mimetypes.guess_type(filename)) - return response + return sendfile(request, local_path) @has_request_variables def serve_file_backend(request, user_profile, realm_id_str, filename): diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 576762e250794..d32db73b21c0c 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -52,3 +52,6 @@ # Don't require anything about password strength in development PASSWORD_MIN_LENGTH = 0 PASSWORD_MIN_ZXCVBN_QUALITY = 0 +# Make sendfile use django to serve files in development +SENDFILE_BACKEND = 'sendfile.backends.development' +SENDFILE_ROOT = os.path.join(LOCAL_UPLOADS_DIR, 'files') diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index 6c5871d66ce38..399e615554739 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -171,21 +171,20 @@ # directly on the Zulip server. If file storage in Amazon S3 is # desired, you can configure that as follows: # -# (1) Set s3_key and s3_secret_key in /etc/zulip/zulip-secrets.conf to +# Set s3_key and s3_secret_key in /etc/zulip/zulip-secrets.conf to # be the S3 access and secret keys that you want to use, and setting # the S3_AUTH_UPLOADS_BUCKET and S3_AVATAR_BUCKET to be the S3 buckets # you've created to store file uploads and user avatars, respectively. # Then restart Zulip (scripts/restart-zulip). -# -# (2) Edit /etc/nginx/sites-available/zulip-enterprise to comment out -# the nginx configuration for /user_uploads and /user_avatars (see -# https://github.com/zulip/zulip/issues/291 for discussion of a better -# solution that won't be automatically reverted by the Zulip upgrade -# script), and then restart nginx. LOCAL_UPLOADS_DIR = "/home/zulip/uploads" #S3_AUTH_UPLOADS_BUCKET = "" #S3_AVATAR_BUCKET = "" +# Setup the version of NGINX which is being used for this installation. +# By default a NGINX version is automatically detected from system but it could also be +# specified here optionally. +# NGINX_VERSION = '1.4.6' + # Maximum allowed size of uploaded files, in megabytes. DO NOT SET # ABOVE 80MB. The file upload implementation doesn't support chunked # uploads, so browsers will crash if you try uploading larger files. diff --git a/zproject/settings.py b/zproject/settings.py index 315733051f694..d87016484d88d 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -15,6 +15,8 @@ import platform import time import sys +import subprocess +import re import six.moves.configparser from zerver.lib.db import TimeTrackingConnection @@ -93,6 +95,11 @@ def get_secret(key): else: from .dev_settings import * +if PRODUCTION and LOCAL_UPLOADS_DIR and 'NGINX_VERSION' not in vars(): + NGINX_VERSION = re.search(r'([\d.]+)', subprocess.Popen(['nginx', '-v'], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT).stdout.read().decode('utf-8')).group(1) + ######################################################################## # DEFAULT VALUES FOR SETTINGS ######################################################################## @@ -138,6 +145,10 @@ def get_secret(key): 'RATE_LIMITING': True, 'REDIS_HOST': '127.0.0.1', 'REDIS_PORT': 6379, + 'SENDFILE_BACKEND': 'sendfile.backends.nginx', + 'SENDFILE_ROOT': '/home/zulip/uploads/files', + 'SENDFILE_URL': '/serve_uploads', + 'NGINX_VERSION': None, # The following bots only exist in non-VOYAGER installs 'ERROR_BOT': None, 'NEW_USER_BOT': None,