Skip to content

Commit

Permalink
Merge pull request #2 from plone-security/folder-contents-escape-or-h…
Browse files Browse the repository at this point in the history
…tml-safe

Folder contents: escape or html safe
  • Loading branch information
mauritsvanrees committed Jun 28, 2021
2 parents bcd4b11 + 683dae5 commit 9ad8c6e
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 67 deletions.
3 changes: 3 additions & 0 deletions Products/PloneHotfix20210518/CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
1.5 (unreleased)
----------------


- Fixed new XSS vulnerability in folder contents on Plone 5.0 and higher.

- Added support for environment variable ``STRICT_TRAVERSE_CHECK``.

- Default value is 0, which means as strict as the code from version 1.4.
Expand Down
3 changes: 3 additions & 0 deletions Products/PloneHotfix20210518/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ This hotfix fixes several security issues:
Reported by MisakiKata and David Miller.
- Server Side Request Forgery via lxml parser.
Reported by MisakiKata and David Miller.
- XSS in folder contents on Plone 5.0 and higher.
Reported by Matt.
Only included since version 1.5 of the hotfix.


Compatibility
Expand Down
11 changes: 11 additions & 0 deletions Products/PloneHotfix20210518/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
HAS_PLONE = False
else:
from Products.CMFPlone import patches # noqa
# If the 2020 hotfix is available, we want to load it first as well,
# especially for the 'content' patch.
# In general, it is advisable for users to put the oldest hotfix first in the eggs.
try:
pkg_resources.get_distribution("Products.PloneHotfix20200121")
except pkg_resources.DistributionNotFound:
pass
else:
import Products.PloneHotfix20200121 # noqa


# General hotfixes for all, including Zope/CMF.
hotfixes = [
Expand Down Expand Up @@ -64,6 +74,7 @@
hotfixes.append("publishing")
hotfixes.append("qi")
hotfixes.append("transforms")
hotfixes.append("content")

# Apply the fixes
for hotfix in hotfixes:
Expand Down
4 changes: 4 additions & 0 deletions Products/PloneHotfix20210518/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
# integer_types = (int,)
text_type = str
# binary_type = bytes
# six has no char_types, but it should:
# bytes + string + unicode, where defined.
char_types = (bytes, str)
else:
string_types = (basestring,)
# integer_types = (int, long)
text_type = unicode
# binary_type = str
char_types = string_types
50 changes: 50 additions & 0 deletions Products/PloneHotfix20210518/content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from ._compat import char_types
from .htmltools import html_escape
from .htmltools import html_safe

import json

try:
# plone.app.content 3
from plone.app.content.browser import contents as fc
except ImportError:
try:
# 2.2.x
from plone.app.content.browser import folder as fc
except ImportError:
# 2.1.x and lower do not need this patch
fc = None


if fc is not None:
# Patch ContextInfo
# If PloneHotfix20200121 is loaded, then the original call will already
# have been saved under a different name.
# We patch that one, instead of letting our patch call a patch which calls the original.
orig_name = "_orig___call__"
if not hasattr(fc.ContextInfo, orig_name):
setattr(fc.ContextInfo, orig_name, fc.ContextInfo.__call__)

def context_info_call(self):
result = self._orig___call__()
data = json.loads(result)
obj = data.get("object", None)
if obj is None:
return result
changed = False
for key, value in obj.items():
if not isinstance(value, char_types):
continue
safe_value = html_safe(value)
if safe_value == value:
continue
obj[key] = safe_value
changed = True
if not changed:
return result
result = json.dumps(data)
return result


fc.ContextInfo.__call__ = context_info_call
69 changes: 2 additions & 67 deletions Products/PloneHotfix20210518/difftool.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from ._compat import PY2
from ._compat import text_type
from .htmltools import html_escape
from .htmltools import html_safe
from os import linesep
from Products.CMFDiffTool.libs import htmldiff
from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.transforms.safe_html import SafeHTML

import Products.CMFDiffTool

Expand All @@ -19,7 +17,6 @@
make_lists_same_length = None



try:
# If this import works, this is already a fixed version.
from Products.CMFDiffTool.utils import html_escape as already_patched
Expand All @@ -28,68 +25,6 @@

if not already_patched:


try:
from html import escape
except ImportError:
from cgi import escape


def safe_unicode(value):
# This is like Products.CMFPlone.utils.safe_unicode, but in this case we always
# ruturn unicode, also when we get an integer as input.
if isinstance(value, text_type):
return value
try:
value = text_type(value)
except UnicodeDecodeError:
value = value.decode('utf-8', 'replace')
return value


def safe_utf8(value):
return safe_unicode(value).encode('utf-8')


def scrub_html(value):
# Strip illegal HTML tags from string text.
transform = SafeHTML()
# Available in Plone 5.2:
# return transform.scrub_html(value)
data = datastream("text/x-html-safe")
data = transform.convert(value, data)
return data.getData()

# We will have two functions:
# - html_escape: escape html, for example turn '<' into '&lt;'
# - html_safe: return html with dangerous tags removed, using safe html transform.
#
# In both Python 2 and 3, the convert function that we use in safe_html
# cannot handle a non string-like value, for example an integer.
# Same is true for the escape function.
# Seems good to always return a string-like value though.
# But should that be bytes or string or unicode?
if PY2:
# We use this in places where the result gets inserted in a string/bytes,
# so we should use a string (utf-8) here.
def html_escape(value):
value = safe_utf8(value)
return escape(value, 1)

def html_safe(value):
value = safe_utf8(value)
return scrub_html(value)
else:
# In Python 3 this gets inserted in a string/text.
def html_escape(value):
value = safe_unicode(value)
return escape(value, 1)

def html_safe(value):
value = safe_unicode(value)
return scrub_html(value)


def binary_inline_diff(self):
"""Simple inline diff that just checks that the filename
has changed."""
Expand Down
65 changes: 65 additions & 0 deletions Products/PloneHotfix20210518/htmltools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from ._compat import PY2
from ._compat import text_type
from Products.PortalTransforms.data import datastream
from Products.PortalTransforms.transforms.safe_html import SafeHTML

try:
from html import escape
except ImportError:
from cgi import escape


def safe_unicode(value):
# This is like Products.CMFPlone.utils.safe_unicode, but in this case we always
# ruturn unicode, also when we get an integer as input.
if isinstance(value, text_type):
return value
try:
value = text_type(value)
except UnicodeDecodeError:
value = value.decode('utf-8', 'replace')
return value


def safe_utf8(value):
return safe_unicode(value).encode('utf-8')


def scrub_html(value):
# Strip illegal HTML tags from string text.
transform = SafeHTML()
# Available in Plone 5.2:
# return transform.scrub_html(value)
data = datastream("text/x-html-safe")
data = transform.convert(value, data)
return data.getData()


# We will have two functions:
# - html_escape: escape html, for example turn '<' into '&lt;'
# - html_safe: return html with dangerous tags removed, using safe html transform.
#
# In both Python 2 and 3, the convert function that we use in safe_html
# cannot handle a non string-like value, for example an integer.
# Same is true for the escape function.
# Seems good to always return a string-like value though.
# But should that be bytes or string or unicode?
if PY2:
# We use this in places where the result gets inserted in a string/bytes,
# so we should use a string (utf-8) here.
def html_escape(value):
value = safe_utf8(value)
return escape(value, 1)

def html_safe(value):
value = safe_utf8(value)
return scrub_html(value)
else:
# In Python 3 this gets inserted in a string/text.
def html_escape(value):
value = safe_unicode(value)
return escape(value, 1)

def html_safe(value):
value = safe_unicode(value)
return scrub_html(value)
Loading

0 comments on commit 9ad8c6e

Please sign in to comment.