Repository: saleor/saleor

Issue:
Sanitize output from the rich-text editor
Output produced by the `RichTextEditorWidget` should be sanitized in backend before saving it to the database. The medium-editor used in frontend already does the sanitization, but that doesn't protect us from posting any malicious content manually. I'm not sure if Django provides any sanitization tools, if not, we should use a third-party library.


Discussion:
If we are going to sanitize before saving, we should also consider signing the stored value using item's ID and `SECRET_KEY`, so we can check if wasn't tampered with via, say, malicious sql query aimed to deface the store by changing products descriptions to adult content.
What attack vector do you have in mind? Raw SQL access would likely give you access to more data than you could steal by injecting a cross-site script.
It's the damage control for when you were already compromised but attacker decides to do extra harm to your business or reputation by, say, replacing your products descriptions with iframes to adult sites.
I believe if your site is compromised and your database contents are replaced, the actual content of your site is the least of your problems as you'll have to shut it down anyway.
Absolutely, but for your brand manger this makes difference between *minutes* of having store with products renamed to something offensive vs *minutes* with store which products descriptions contain some graphic stuff, thats far more likely to go viral and harmful for your brand.

*Yeah... I've been there and it made me paranoid* ;)
I see our role as preventing fraud and money loss, not as protection from defacement until the platform we build becomes a viable attack vector. If I have direct access to the database I can do many things including elevating my own user to admin privileges and uploading terrible stuff using the official dashboard's interface.
Do you recommend any sanitizer for Python? [Bleach](https://github.com/mozilla/bleach) seems quite popular.

Resolving patch:
diff --git a/saleor/dashboard/product/forms.py b/saleor/dashboard/product/forms.py
--- a/saleor/dashboard/product/forms.py
+++ b/saleor/dashboard/product/forms.py
@@ -1,3 +1,5 @@
+import bleach
+from bleach.sanitizer import ALLOWED_TAGS as BLEACH_ALLOWED_TAGS
 from django import forms
 from django.db.models import Count
 from django.forms.models import ModelChoiceIterator
@@ -14,6 +16,18 @@
 from .widgets import ImagePreviewWidget
 
 
+class RichTextField(forms.CharField):
+    """A field for rich text editor, providing backend sanitization."""
+
+    widget = RichTextEditorWidget
+    ALLOWED_TAGS = BLEACH_ALLOWED_TAGS + ['br', 'p', 'h2', 'h3']
+
+    def to_python(self, value):
+        value = super().to_python(value)
+        value = bleach.clean(value, tags=self.ALLOWED_TAGS)
+        return value
+
+
 class ProductTypeSelectorForm(forms.Form):
     """Form that allows selecting product type."""
 
@@ -131,6 +145,7 @@ class Meta:
 
     collections = forms.ModelMultipleChoiceField(
         required=False, queryset=Collection.objects.all())
+    description = RichTextField()
 
     def __init__(self, *args, **kwargs):
         self.product_attributes = []
@@ -140,7 +155,6 @@ def __init__(self, *args, **kwargs):
         self.product_attributes = self.product_attributes.prefetch_related(
             'values')
         self.prepare_fields_for_attributes()
-        self.fields['description'].widget = RichTextEditorWidget()
         self.fields["collections"].initial = Collection.objects.filter(
             products__name=self.instance)
 


Test patch:
diff --git a/tests/dashboard/test_product.py b/tests/dashboard/test_product.py
--- a/tests/dashboard/test_product.py
+++ b/tests/dashboard/test_product.py
@@ -7,6 +7,7 @@
 from django.conf import settings
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.forms import HiddenInput
+from django.forms.models import model_to_dict
 from django.urls import reverse
 from django.utils.encoding import smart_text
 import pytest
@@ -666,7 +667,6 @@ def test_hide_field_in_variant_choice_field_form():
 def test_assign_collection_to_product(product_in_stock):
     product = product_in_stock
     collection = Collection.objects.create(name='test_collections')
-    data = product.__dict__
     data = {
         'name': product.name,
         'price': product.price.gross,
@@ -678,3 +678,23 @@ def test_assign_collection_to_product(product_in_stock):
     form.save()
     assert product.collections.first().name == 'test_collections'
     assert collection.products.first().name == product.name
+
+
+def test_sanitize_product_description(product_type, default_category):
+    product = Product.objects.create(
+        name='Test Product', price=10, description='', pk=10,
+        product_type=product_type, category=default_category)
+    data = model_to_dict(product)
+    data['description'] = (
+        '<b>bold</b><p><i>italic</i></p><h2>Header</h2><h3>subheader</h3>'
+        '<blockquote>quote</blockquote>'
+        '<p><a href="www.mirumee.com">link</a></p>'
+        '<p>an <script>evil()</script>example</p>')
+    data['price'] = 20
+    form = ProductForm(data, instance=product)
+    assert form.is_valid()
+    form.save()
+    assert product.description == (
+        '<b>bold</b><p><i>italic</i></p><h2>Header</h2><h3>subheader</h3>'
+        '<blockquote>quote</blockquote><p><a href="www.mirumee.com">link</a></p>'
+        '<p>an &lt;script&gt;evil()&lt;/script&gt;example</p>')