escape syndication target before displaying

closes possible XSS vector

fixes #51
This commit is contained in:
Kyle Mahan 2016-05-06 14:33:42 -07:00
parent 8cb2124251
commit 93e751c058
4 changed files with 30 additions and 13 deletions

View file

@ -1,7 +1,7 @@
import flask import flask
import flask.ext.login as flask_login import flask.ext.login as flask_login
import requests import requests
from woodwind import util
api = flask.Blueprint('api', __name__) api = flask.Blueprint('api', __name__)
@ -13,6 +13,9 @@ def publish():
content = flask.request.form.get('content') content = flask.request.form.get('content')
syndicate_to = flask.request.form.getlist('syndicate-to[]') syndicate_to = flask.request.form.getlist('syndicate-to[]')
if syndicate_to:
syndicate_to = [util.html_unescape(id) for id in syndicate_to]
data = { data = {
'h': 'entry', 'h': 'entry',
'syndicate-to[]': syndicate_to, 'syndicate-to[]': syndicate_to,

View file

@ -23,7 +23,7 @@
{% for target in current_user.get_setting('syndicate-to', []) %} {% for target in current_user.get_setting('syndicate-to', []) %}
<div class="syndication-toggle"> <div class="syndication-toggle">
<input id="sc-{{entry.id}}-{{loop.index}}" type="checkbox" name="syndicate-to[]" value="{{ target | syndication_target_id }}"{% if entry is syndicated_to(target) %} checked{% endif %} /> <input id="sc-{{entry.id}}-{{loop.index}}" type="checkbox" name="syndicate-to[]" value="{{ target | render_syndication_target_id }}"{% if entry is syndicated_to(target) %} checked{% endif %} />
<label for="sc-{{entry.id}}-{{loop.index}}">{{ target | render_syndication_target }}</label> <label for="sc-{{entry.id}}-{{loop.index}}">{{ target | render_syndication_target }}</label>
</div> </div>

View file

@ -1,5 +1,6 @@
import pickle import pickle
import re import re
from xml.sax import saxutils
from flask import current_app from flask import current_app
from redis import StrictRedis from redis import StrictRedis
@ -56,3 +57,13 @@ def clean(text):
if text is not None: if text is not None:
text = re.sub('<script.*?</script>', '', text, flags=re.DOTALL) text = re.sub('<script.*?</script>', '', text, flags=re.DOTALL)
return bleach.clean(text, strip=True) return bleach.clean(text, strip=True)
def html_escape(text):
# https://wiki.python.org/moin/EscapingHtml
return saxutils.escape(text, {'"': '&quot;', "'": '&apos;'})
def html_unescape(text):
# https://wiki.python.org/moin/EscapingHtml
return saxutils.unescape(text, {'&quot;': '"', '&apos;': "'"})

View file

@ -16,7 +16,6 @@ import pyquerystring
import requests import requests
import re import re
import urllib import urllib
import cgi
import sqlalchemy import sqlalchemy
import sqlalchemy.sql.expression import sqlalchemy.sql.expression
@ -257,11 +256,11 @@ def login():
@micropub.authenticated_handler @micropub.authenticated_handler
def login_callback(resp): def login_callback(resp):
if not resp.me: if not resp.me:
flask.flash(cgi.escape('Login error: ' + resp.error)) flask.flash(util.html_escape('Login error: ' + resp.error))
return flask.redirect(flask.url_for('.index')) return flask.redirect(flask.url_for('.index'))
if resp.error: if resp.error:
flask.flash(cgi.escape('Warning: ' + resp.error)) flask.flash(util.html_escape('Warning: ' + resp.error))
user = load_user(resp.me) user = load_user(resp.me)
if not user: if not user:
@ -287,12 +286,12 @@ def authorize():
@micropub.authorized_handler @micropub.authorized_handler
def micropub_callback(resp): def micropub_callback(resp):
if not resp.me or resp.error: if not resp.me or resp.error:
flask.flash(cgi.escape('Authorize error: ' + resp.error)) flask.flash(util.html_escape('Authorize error: ' + resp.error))
return flask.redirect(flask.url_for('.index')) return flask.redirect(flask.url_for('.index'))
user = load_user(resp.me) user = load_user(resp.me)
if not user: if not user:
flask.flash(cgi.escape('Unknown user for url: ' + resp.me)) flask.flash(util.html_escape('Unknown user for url: ' + resp.me))
return flask.redirect(flask.url_for('.index')) return flask.redirect(flask.url_for('.index'))
user.micropub_endpoint = resp.micropub_endpoint user.micropub_endpoint = resp.micropub_endpoint
@ -764,21 +763,25 @@ def font_awesome_class_for_service(service):
return 'fa fa-send' return 'fa fa-send'
@views.app_template_filter('syndication_target_id') @views.app_template_filter('render_syndication_target_id')
def syndication_target_id(target): def render_syndication_target_id(target):
if isinstance(target, dict): if isinstance(target, dict):
return target.get('uid') or target.get('id') id = target.get('uid') or target.get('id')
return target else:
id = target
return util.html_escape(id)
@views.app_template_filter('render_syndication_target') @views.app_template_filter('render_syndication_target')
def render_syndication_target(target): def render_syndication_target(target):
if isinstance(target, dict): if isinstance(target, dict):
full_name = target.get('name') full_name = target.get('name')
return full_name return util.html_escape(full_name)
return '<img src="{}" alt="{}" />&nbsp;{}'.format( return '<img src="{}" alt="{}" />&nbsp;{}'.format(
favicon_for_url(target), target, prettify_url(target)) favicon_for_url(target),
util.html_escape(target),
util.html_escape(prettify_url(target)))
@views.app_template_test('syndicated_to') @views.app_template_test('syndicated_to')