Compare commits

..

2 Commits
2.0.7 ... 2.0.8

Author SHA1 Message Date
Tim Abbott
726ab9c4fa Release Zulip Server 2.0.8. 2019-12-12 17:15:53 -08:00
Anders Kaseorg
b7c87a4d82 CVE-2019-19775: Close open redirect in thumbnail view.
This closes an open redirect vulnerability, one case of which was
found by Graham Bleaney and Ibrahim Mohamed using Pysa.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2019-12-12 17:14:00 -08:00
6 changed files with 32 additions and 13 deletions

View File

@@ -52,7 +52,7 @@ author = 'The Zulip Team'
# The short X.Y version. # The short X.Y version.
version = '2.0' version = '2.0'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '2.0.7' release = '2.0.8'
# This allows us to insert a warning that appears only on an unreleased # This allows us to insert a warning that appears only on an unreleased
# version, e.g. to say that something is likely to have changed. # version, e.g. to say that something is likely to have changed.

View File

@@ -7,6 +7,10 @@ All notable changes to the Zulip server are documented in this file.
This section lists notable unreleased changes; it is generally updated This section lists notable unreleased changes; it is generally updated
in bursts. in bursts.
### 2.0.8 -- 2019-12-12
- CVE-2019-19775: Close open redirect in thumbnail view.
### 2.0.7 -- 2019-11-21 ### 2.0.7 -- 2019-11-21
- CVE-2019-18933: Fix insecure account creation via social authentication. - CVE-2019-18933: Fix insecure account creation via social authentication.

View File

@@ -1,6 +1,6 @@
ZULIP_VERSION = "2.0.7" ZULIP_VERSION = "2.0.8"
LATEST_MAJOR_VERSION = "2.0" LATEST_MAJOR_VERSION = "2.0"
LATEST_RELEASE_VERSION = "2.0.7" LATEST_RELEASE_VERSION = "2.0.8"
LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2019/03/01/zulip-2-0-released/" LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2019/03/01/zulip-2-0-released/"
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision

View File

@@ -9,6 +9,7 @@ import markdown
import logging import logging
import traceback import traceback
import urllib import urllib
import urllib.parse
import re import re
import os import os
import html import html
@@ -553,7 +554,7 @@ class InlineHttpsProcessor(markdown.treeprocessors.Treeprocessor):
found_imgs = walk_tree(root, lambda e: e if e.tag == "img" else None) found_imgs = walk_tree(root, lambda e: e if e.tag == "img" else None)
for img in found_imgs: for img in found_imgs:
url = img.get("src") url = img.get("src")
if not url.startswith("http://"): if urllib.parse.urlsplit(url).scheme != "http":
# Don't rewrite images on our own site (e.g. emoji). # Don't rewrite images on our own site (e.g. emoji).
continue continue
img.set("src", get_camo_url(url)) img.set("src", get_camo_url(url))

View File

@@ -4,6 +4,7 @@ import base64
import os import os
import sys import sys
import urllib import urllib
from urllib.parse import urljoin, urlsplit, urlunsplit
from django.conf import settings from django.conf import settings
from libthumbor import CryptoURL from libthumbor import CryptoURL
@@ -19,7 +20,8 @@ def is_thumbor_enabled() -> bool:
return settings.THUMBOR_URL != '' return settings.THUMBOR_URL != ''
def user_uploads_or_external(url: str) -> bool: def user_uploads_or_external(url: str) -> bool:
return url.startswith('http') or url.lstrip('/').startswith('user_uploads/') u = urlsplit(url)
return u.scheme != "" or u.netloc != "" or u.path.startswith("/user_uploads/")
def get_source_type(url: str) -> str: def get_source_type(url: str) -> str:
if not url.startswith('/user_uploads/'): if not url.startswith('/user_uploads/'):
@@ -33,16 +35,16 @@ def get_source_type(url: str) -> str:
def generate_thumbnail_url(path: str, def generate_thumbnail_url(path: str,
size: str='0x0', size: str='0x0',
is_camo_url: bool=False) -> str: is_camo_url: bool=False) -> str:
if not (path.startswith('https://') or path.startswith('http://')): path = urljoin("/", path)
path = '/' + path u = urlsplit(path)
if not is_thumbor_enabled(): if not is_thumbor_enabled():
if path.startswith('http://'): if u.scheme == "" and u.netloc == "":
return urlunsplit(u)
return get_camo_url(path) return get_camo_url(path)
return path
if not user_uploads_or_external(path): if u.scheme == "" and u.netloc == "" and not u.path.startswith("/user_uploads/"):
return path return urlunsplit(u)
source_type = get_source_type(path) source_type = get_source_type(path)
safe_url = base64.urlsafe_b64encode(path.encode()).decode('utf-8') safe_url = base64.urlsafe_b64encode(path.encode()).decode('utf-8')

View File

@@ -148,6 +148,9 @@ class ThumbnailTest(ZulipTestCase):
image_url = 'http://images.foobar.com/12345' image_url = 'http://images.foobar.com/12345'
run_test_with_image_url(image_url) run_test_with_image_url(image_url)
image_url = '//images.foobar.com/12345'
run_test_with_image_url(image_url)
def test_local_file_type(self) -> None: def test_local_file_type(self) -> None:
def get_file_path_urlpart(uri: str, size: str='') -> str: def get_file_path_urlpart(uri: str, size: str='') -> str:
url_in_result = 'smart/filters:no_upscale()%s/%s/source_type/local_file' url_in_result = 'smart/filters:no_upscale()%s/%s/source_type/local_file'
@@ -281,7 +284,8 @@ class ThumbnailTest(ZulipTestCase):
with self.settings(THUMBOR_URL=''): with self.settings(THUMBOR_URL=''):
result = self.client_get("/thumbnail?url=%s&size=full" % (quoted_uri)) result = self.client_get("/thumbnail?url=%s&size=full" % (quoted_uri))
self.assertEqual(result.status_code, 302, result) self.assertEqual(result.status_code, 302, result)
self.assertEqual(uri, result.url) base = 'https://external-content.zulipcdn.net/external_content/56c362a24201593891955ff526b3b412c0f9fcd2/68747470733a2f2f7777772e676f6f676c652e636f6d2f696d616765732f737270722f6c6f676f34772e706e67'
self.assertEqual(base, result.url)
uri = 'http://www.google.com/images/srpr/logo4w.png' uri = 'http://www.google.com/images/srpr/logo4w.png'
quoted_uri = urllib.parse.quote(uri, safe='') quoted_uri = urllib.parse.quote(uri, safe='')
@@ -291,6 +295,14 @@ class ThumbnailTest(ZulipTestCase):
base = 'https://external-content.zulipcdn.net/external_content/7b6552b60c635e41e8f6daeb36d88afc4eabde79/687474703a2f2f7777772e676f6f676c652e636f6d2f696d616765732f737270722f6c6f676f34772e706e67' base = 'https://external-content.zulipcdn.net/external_content/7b6552b60c635e41e8f6daeb36d88afc4eabde79/687474703a2f2f7777772e676f6f676c652e636f6d2f696d616765732f737270722f6c6f676f34772e706e67'
self.assertEqual(base, result.url) self.assertEqual(base, result.url)
uri = '//www.google.com/images/srpr/logo4w.png'
quoted_uri = urllib.parse.quote(uri, safe='')
with self.settings(THUMBOR_URL=''):
result = self.client_get("/thumbnail?url=%s&size=full" % (quoted_uri,))
self.assertEqual(result.status_code, 302, result)
base = 'https://external-content.zulipcdn.net/external_content/676530cf4b101d56f56cc4a37c6ef4d4fd9b0c03/2f2f7777772e676f6f676c652e636f6d2f696d616765732f737270722f6c6f676f34772e706e67'
self.assertEqual(base, result.url)
def test_with_different_THUMBOR_URL(self) -> None: def test_with_different_THUMBOR_URL(self) -> None:
self.login(self.example_email("hamlet")) self.login(self.example_email("hamlet"))
fp = StringIO("zulip!") fp = StringIO("zulip!")