Skip to content

Commit 8130f74

Browse files
committed
[#183] Fix possible XSS by sanitizing HTML content
1 parent ec1dfdd commit 8130f74

6 files changed

Lines changed: 50 additions & 26 deletions

File tree

pytition/petition/helpers.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import requests
2+
import lxml
3+
from lxml.html.clean import Cleaner
24
from django.http import Http404, HttpResponseForbidden
35
from django.conf import settings
46
from django.urls import reverse
@@ -7,10 +9,20 @@
79
from django.core.mail import get_connection, EmailMultiAlternatives, EmailMessage
810
from django.utils.translation import ugettext as _
911
from django.contrib.auth.models import User
10-
from .models import PytitionUser, Petition
11-
from .forms import UpdateInfoForm
1212

1313

14+
# Remove all javascripts from HTML code
15+
def sanitize_html(unsecure_html_content):
16+
cleaner = Cleaner(inline_style=False, scripts=True, javascript=True,
17+
safe_attrs=lxml.html.defs.safe_attrs | set(['style']),
18+
frames=False, embedded=False,
19+
meta=True, links=True, page_structure=True)
20+
try:
21+
secure_html_content = lxml.html.tostring(cleaner.clean_html(lxml.html.fromstring(unsecure_html_content)), method="html")
22+
except:
23+
secure_html_content = b''
24+
return secure_html_content.decode()
25+
1426
# Get the client IP address, considering proxies and RP
1527
def get_client_ip(request):
1628
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
@@ -22,6 +34,7 @@ def get_client_ip(request):
2234

2335
# Get the user of the current session
2436
def get_session_user(request):
37+
from .models import PytitionUser
2538
try:
2639
pytitionuser = PytitionUser.objects.get(user__username=request.user.username)
2740
except User.DoesNotExist:
@@ -38,6 +51,7 @@ def check_user_in_orga(user, orga):
3851

3952
# Return a 404 if a petition does not exist
4053
def petition_from_id(id):
54+
from .models import Petition
4155
petition = Petition.by_id(id)
4256
if petition is None:
4357
raise Http404(_("Petition does not exist"))
@@ -114,6 +128,7 @@ def subscribe_to_newsletter(petition, email):
114128
connection=connection).send(fail_silently=True)
115129

116130
def get_update_form(user, data=None):
131+
from .forms import UpdateInfoForm
117132
if not data:
118133
_data = {
119134
'first_name': user.first_name,

pytition/petition/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
from django.contrib.auth.hashers import get_hasher
1111
from django.db import transaction
1212
from django.urls import reverse
13-
from django.db.models import Q
14-
1513

1614
from tinymce import models as tinymce_models
1715
from colorfield.fields import ColorField
1816

17+
from .helpers import sanitize_html
18+
1919
import html
2020

2121

@@ -298,11 +298,11 @@ def signature_number(self):
298298

299299
@property
300300
def raw_twitter_description(self):
301-
return html.unescape(mark_safe(strip_tags(self.twitter_description)))
301+
return html.unescape(mark_safe(strip_tags(sanitize_html(self.twitter_description))))
302302

303303
@property
304304
def raw_text(self):
305-
return html.unescape(mark_safe(strip_tags(self.text)))
305+
return html.unescape(mark_safe(strip_tags(sanitize_html(self.text))))
306306

307307
def __str__(self):
308308
return self.title
@@ -363,7 +363,7 @@ def save(self, *args, **kwargs):
363363
if not self.salt:
364364
hasher = get_hasher()
365365
self.salt = hasher.salt().decode('utf-8')
366-
super(Petition, self).save(*args, **kwargs)
366+
super(Petition, self).save(*args, **kwargs)
367367

368368

369369
# --------------------------------- Signature ---------------------------------

pytition/petition/templates/petition/petition_detail.html

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22
{% load static %}
33
{% load i18n %}
44
{% load widget_tweaks %}
5+
{% load petition_extras %}
56

6-
{% block title %}{% autoescape off %}{{ petition.title }}{% endautoescape %}{% endblock title %}
7+
{% block title %}{{ petition.title }}{% endblock title %}
78

89
<!-- Extra meta -->
910
{% block extrameta %}
10-
<meta name="description" content="{% autoescape off %}{{ petition.title }}{% endautoescape %}"/>
11+
<meta name="description" content="{{ petition.title }}"/>
1112

12-
<meta property="og:title" content="{% autoescape off %}{{ petition.title }}{% endautoescape %}"/>
13+
<meta property="og:title" content="{{ petition.title }}"/>
1314
{% if petition.twitter_description %}
1415
<meta property="og:description"
15-
content="{% autoescape off%}{{ petition.raw_twitter_description }}{% endautoescape %}"/>
16+
content="{{ petition.raw_twitter_description|safe }}"/>
1617
{% else %}
17-
<meta property="og:description" content="{% autoescape off %}{{ petition.raw_text }}{% endautoescape %}"/>
18+
<meta property="og:description" content="{{ petition.raw_text|safe }}"/>
1819
{% endif %}
1920
<meta property="og:type" content="website"/>
2021
<meta property="og:url" content="{{ meta.petition_url }}" />
@@ -23,24 +24,24 @@
2324
<meta property="og:image" content="{{ petition.twitter_image }}"/>
2425
{% endif %}
2526

26-
<meta itemprop="name" content="{% autoescape off %}{{ petition.title }}{% endautoescape %}"/>
27+
<meta itemprop="name" content="{{ petition.title }}"/>
2728
{% if petition.twitter_description %}
2829
<meta itemprop="description"
29-
content="{% autoescape off %}{{ petition.raw_twitter_description }}{% endautoescape %}"/>
30+
content="{{ petition.raw_twitter_description|safe }}"/>
3031
{% else %}
31-
<meta itemprop="description" content="{% autoescape off %}{{ petition.raw_text }}{% endautoescape %}"/>
32+
<meta itemprop="description" content="{{ petition.raw_text|safe }}"/>
3233
{% endif %}
3334
<!-- FIXME: <meta itemprop="url" content=""/> //-->
3435

3536
<meta name="twitter:card" content="summary"/>
3637
<meta name="twitter:creator" content="@yannsionneau"/>
37-
<meta name="twitter:site" content="{% autoescape off %}{{ petition.org_twitter_handle }}{% endautoescape %}"/>
38-
<meta name="twitter:title" content="{% autoescape off %}{{ petition.title }}{% endautoescape %}"/>
38+
<meta name="twitter:site" content="{{ petition.org_twitter_handle }}"/>
39+
<meta name="twitter:title" content="{{ petition.title }}"/>
3940
{% if petition.twitter_description %}
4041
<meta name="twitter:description"
41-
content="{% autoescape off %}{{ petition.raw_twitter_description }}{% endautoescape %}"/>
42+
content="{{ petition.raw_twitter_description|safe }}"/>
4243
{% else %}
43-
<meta name="twitter:description" content="{% autoescape off %}{{ petition.raw_text }}{% endautoescape %}"/>
44+
<meta name="twitter:description" content="{{ petition.raw_text|safe }}"/>
4445
{% endif %}
4546
{% if petition.twitter_image %}
4647
<meta name="twitter:image" content="{{ petition.twitter_image }}"/>
@@ -86,15 +87,15 @@
8687

8788
<div class="container">
8889
<div class="jumbotron text-center">
89-
<h1 class="jumbotron-heading">{% autoescape off%}{{ petition.title|striptags }}{% endautoescape %}</h1>
90+
<h1 class="jumbotron-heading">{{ petition.title|html_sanitize|striptags|safe }}</h1>
9091
</div>
9192
<div class="petition-wrapper">
9293
<div class="content">
9394
<div class="formular" id="petition">
9495
<div class="form-wrapper">
9596
{% if petition.side_text %}
9697
<p class="intro" id="intro">
97-
{% autoescape off%}{{ petition.side_text }}{% endautoescape %}
98+
{{ petition.side_text|html_sanitize|safe }}
9899
</p>
99100
{% endif %}
100101
<p class="sign text-primary"><strong>Signez la pétition&nbsp;!</strong></p>
@@ -176,7 +177,7 @@ <h1 class="jumbotron-heading">{% autoescape off%}{{ petition.title|striptags }}
176177
</div>
177178
</div>
178179
<div class="presentation">
179-
{% autoescape off%}{{ petition.text }}{% endautoescape %}
180+
{{ petition.text|html_sanitize|safe }}
180181
</div>
181182
</div>
182183
</div>
@@ -193,10 +194,10 @@ <h1 class="jumbotron-heading">{% autoescape off%}{{ petition.title|striptags }}
193194
<div class="footer-wrapper bg-dark">
194195
<footer role="contentinfo" class="footer">
195196
<div class="footer-links">
196-
{% autoescape off %}{{ petition.footer_links }}{% endautoescape %}
197+
{{ petition.footer_links|html_sanitize|safe }}
197198
</div>
198199
<div class="footer-text">
199-
{% autoescape off %}{{ petition.footer_text }}{% endautoescape %}
200+
{{ petition.footer_text|html_sanitize|safe }}
200201
</div>
201202
</footer>
202203
</div>

pytition/petition/templatetags/petition_extras.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import template
2+
from petition.helpers import sanitize_html
23

34
register = template.Library()
45

@@ -24,4 +25,8 @@ def bootstrap(field):
2425
return add_class(field, "form-check-input")
2526
if widget == "fileinput":
2627
return add_class(field, "form-control-file")
27-
return field
28+
return field
29+
30+
@register.filter
31+
def html_sanitize(html):
32+
return sanitize_html(html)

pytition/pytition/settings/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@
190190
'insert_toolbar': 'forecolor backcolor',
191191
'fontsize_formats': '8pt 10pt 12pt 14pt 18pt 24pt 36pt',
192192
'entity_encoding': 'raw',
193+
'relative_urls' : False,
194+
'convert_urls': True,
193195
'setup': """function(ed) {
194196
ed.on('change', function(e) {
195197
set_mce_changed(ed);

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ mysqlclient==1.3.13
77
django-widget-tweaks==1.4.3
88
beautifulsoup4~=4.6.3
99
django-formtools==2.1
10-
bcrypt
10+
bcrypt
11+
lxml

0 commit comments

Comments
 (0)