Commits

Mark Lavin committed 3dc0605 Merge

Merge back stable fixes.

Comments (0)

Files changed (11)

 d1b726b55b9093d058c416a46eb186fccc6280a4 version-0.4.2
 4ca831efa36d0c879cde84921df0641f7806a74e version-0.5.0
 4597983758f1b174c40c0a5c279952925d8d3085 version-0.5.1
+26ccd03bdadc78c1861f63baff3bd9f401f58e5b version-0.5.2

docs/advanced.rst

 
 You might want to help your users by submitting the form once they have selected a valid
 item. To do this you simply need to listen for the ``autocompleteselect`` event. This
-event is fired by the text input which has an index of 0. If you field is named ``my_field``
+event is fired by the text input which has an index of 0. If your field is named ``my_field``
 then input to watch would be ``my_field_0`` such as:
 
     .. code-block:: html
 for some additional detail on this problem.
 
 
-.. _advanaced-label-formats:
+.. _advanced-label-formats:
 
 Label Formats on the Client Side
 --------------------------------------
 # The short X.Y version.
 version = '0.5'
 # The full version, including alpha/beta/rc tags.
-release = '0.5.1'
+release = '0.5.2'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
     :param item: An item from the search results.
     :return: A string representation of the item to be shown in the search results.
         The label can include HTML. For changing the label format on the client side
-        see :ref:`Advanaced Label Formats <advanaced-label-formats>`.
+        see :ref:`Advanced Label Formats <advanced-label-formats>`.
     
 
 .. py:method:: LookupBase.get_item_id(item)
 
     By default ``format_item`` creates a dictionary with the three keys used by
     the UI plugin: id, value, label. These are generated from the calls to
-    ``get_item_id``, ``get_item_value``, and ``get_item_label``. If you want to
+    ``get_item_id``, ``get_item_value`` and ``get_item_label``. If you want to
     add additional keys you should add them here.
 
+    The results of ``get_item_id``, ``get_item_value`` and ``get_item_label`` are
+    conditionally escaped to prevent Cross Site Scripting (XSS) similar to the templating
+    language. If you know that the content is safe and you want to use these methods
+    to include HTML should mark the content as safe with ``django.utils.safestring.mark_safe``
+    inside the ``get_item_*`` methods.
+
     :param item: An item from the search results.
     :return: A dictionary of information for this item to be sent back to the client.
 

docs/releases.rst

 Release Notes
 ==================
 
+v0.5.2 (Released 2012-06-27)
+--------------------------------------
+
+Bug Fixes
+_________________
+
+- Fixed XSS flaw with lookup ``get_item_*`` methods. Thanks slafs for the report.
+- Fixed bug when passing widget instance rather than widget class to ``AutoCompleteSelectField`` or ``AutoCompleteSelectMultipleField``.
+
+
 v0.5.1 (Released 2012-06-08)
 --------------------------------------
 
 - Multiple search fields for :ref:`model based lookups <ModelLookup>`
 - Support for :ref:`highlighting term matches <javascript-highlightMatch>`
 - Support for HTML in :ref:`result labels <lookup-get-item-label>`
-- Support for :ref:`client side formatting <advanaced-label-formats>`
+- Support for :ref:`client side formatting <advanced-label-formats>`
 - Additional documentation
 - Expanded examples in example project
 

docs/settings.rst

 ``formatLabel`` is a function that is run prior to rendering the search results in
 the dropdown menu. It takes two arguments: the current item label and the item data
 dictionary. It should return the label which should be used. For more information
-on its usage see :ref:`Label Formats on the Client Side <advanaced-label-formats>`.
+on its usage see :ref:`Label Formats on the Client Side <advanced-label-formats>`.
 
 Default: ``null``
 

selectable/__init__.py

 __version_info__ = {
     'major': 0,
     'minor': 5,
-    'micro': 1,
+    'micro': 2,
     'releaselevel': 'final',
 }
 

selectable/base.py

 from django.db.models import Q
 from django.utils import simplejson as json
 from django.utils.encoding import smart_unicode
+from django.utils.html import conditional_escape
 from django.utils.translation import ugettext as _
 
 from selectable.forms import BaseLookupForm
 
     def format_item(self, item):
          return {
-            'id': self.get_item_id(item),
-            'value': self.get_item_value(item),
-            'label': self.get_item_label(item)
+            'id': conditional_escape(self.get_item_id(item)),
+            'value': conditional_escape(self.get_item_value(item)),
+            'label': conditional_escape(self.get_item_label(item))
         }
 
     def paginate_results(self, request, results, limit):

selectable/forms/fields.py

         self.lookup_class = lookup_class
         self.allow_new = kwargs.pop('allow_new', False)
         self.limit = kwargs.pop('limit', None)
-        widget = kwargs.pop('widget', self.widget) or self.widget
+        widget = kwargs.get('widget', self.widget) or self.widget
         if isinstance(widget, type):
             kwargs['widget'] = widget(lookup_class, allow_new=self.allow_new, limit=self.limit)
         super(AutoCompleteSelectField, self).__init__(*args, **kwargs)
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = lookup_class
         self.limit = kwargs.pop('limit', None)
-        widget = kwargs.pop('widget', self.widget) or self.widget
+        widget = kwargs.get('widget', self.widget) or self.widget
         if isinstance(widget, type):
             kwargs['widget'] = widget(lookup_class, limit=self.limit)
         super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs)

selectable/tests/base.py

 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.test import TestCase
+from django.utils.html import escape
+from django.utils.safestring import SafeData, mark_safe
 
 from selectable.base import ModelLookup
 from selectable.tests import Thing
 __all__ = (
     'ModelLookupTestCase',
     'MultiFieldLookupTestCase',
+    'LookupEscapingTestCase',
 )
 
 
         item = lookup.get_item(thing.pk)
         self.assertEqual(thing, item)
 
+    def test_format_item_escaping(self):
+        "Id, value and label should be escaped."
+        lookup = self.get_lookup_instance()
+        thing = self.create_thing(data={'name': 'Thing'})
+        item_info = lookup.format_item(thing)
+        self.assertTrue(isinstance(item_info['id'], SafeData))
+        self.assertTrue(isinstance(item_info['value'], SafeData))
+        self.assertTrue(isinstance(item_info['label'], SafeData))
+
 
 class MultiFieldLookup(ModelLookup):
     model = Thing
         qs = lookup.get_query(request=None, term='other')
         self.assertTrue(thing.pk not in qs.values_list('id', flat=True))
         self.assertTrue(other_thing.pk in qs.values_list('id', flat=True))
+
+
+class HTMLLookup(ModelLookup):
+    model = Thing
+    search_fields = ('name__icontains', )
+
+    def get_item_value(self, item):
+        "Not marked as safe."
+        return item.name
+
+    def get_item_label(self, item):
+        "Mark label as safe."
+        return mark_safe(item.name)
+
+
+class LookupEscapingTestCase(BaseSelectableTestCase):
+    lookup_cls = HTMLLookup
+
+    def get_lookup_instance(self):
+        return self.__class__.lookup_cls()
+
+    def test_escape_html(self):
+        "HTML should be escaped by default."
+        lookup = self.get_lookup_instance()
+        bad_name = "<script>alert('hacked');</script>"
+        escaped_name = escape(bad_name)
+        thing = self.create_thing(data={'name': bad_name})
+        item_info = lookup.format_item(thing)
+        self.assertEqual(item_info['value'], escaped_name)
+
+    def test_conditional_escape(self):
+        "Methods should be able to mark values as safe."
+        lookup = self.get_lookup_instance()
+        bad_name = "<script>alert('hacked');</script>"
+        escaped_name = escape(bad_name)
+        thing = self.create_thing(data={'name': bad_name})
+        item_info = lookup.format_item(thing)
+        self.assertEqual(item_info['label'], bad_name)

selectable/tests/fields.py

         field = self.get_field_instance(widget=widget_cls)
         self.assertTrue(isinstance(field.widget, widget_cls))
 
+    def test_alternate_widget_instance(self):
+        widget = widgets.AutoComboboxWidget(self.lookup_cls)
+        field = self.get_field_instance(widget=widget)
+        self.assertTrue(isinstance(field.widget, widgets.AutoComboboxWidget))
+
 
 class AutoComboboxSelectFieldTestCase(BaseFieldTestCase):
     field_cls = fields.AutoComboboxSelectField
         field = self.get_field_instance(widget=widget_cls)
         self.assertTrue(isinstance(field.widget, widget_cls))
 
+    def test_alternate_widget_instance(self):
+        widget = widgets.AutoComboboxSelectMultipleWidget(self.lookup_cls)
+        field = self.get_field_instance(widget=widget)
+        self.assertTrue(isinstance(field.widget, widgets.AutoComboboxSelectMultipleWidget))
+
 
 class AutoComboboxSelectMultipleFieldTestCase(BaseFieldTestCase):
     field_cls = fields.AutoComboboxSelectMultipleField