diff --git a/app/controllers/rss.py b/app/controllers/rss.py index 0facbbc..05fb7e6 100644 --- a/app/controllers/rss.py +++ b/app/controllers/rss.py @@ -1,5 +1,7 @@ """RSS controller for the site's latest activity.""" +import re + from datetime import datetime, timezone from flask import Blueprint, Response, current_app @@ -130,11 +132,16 @@ def rss_activity(): return Response(rss_content, mimetype='application/rss+xml; charset=utf-8') +# XML 1.0 simply forbids these. escaping doesn't make them legal, and one +# \x0b in a crackme name makes every parser reject the whole feed. +_XML_FORBIDDEN = re.compile('[\x00-\x08\x0b\x0c\x0e-\x1f]') + + def escape_xml(text): - """Escape special XML characters.""" + """Escape special XML characters, dropping characters XML 1.0 forbids.""" if text is None: return '' - return (str(text) + return _XML_FORBIDDEN.sub('', str(text) .replace('&', '&') .replace('<', '<') .replace('>', '>') diff --git a/tests/test_rss_and_errors.py b/tests/test_rss_and_errors.py index 4fe0351..b3da227 100644 --- a/tests/test_rss_and_errors.py +++ b/tests/test_rss_and_errors.py @@ -55,6 +55,24 @@ def test_rss_feed_includes_crackmes_solutions_and_solves( assert len(ElementTree.fromstring(response.data).findall('./channel/item')) == 3 +def test_rss_feed_strips_characters_xml_forbids(client, db, sample_crackme): + db.crackme.update_one({'_id': sample_crackme['_id']}, {'$set': { + 'name': 'bad\x0bname', + 'info': 'vertical\ftab\bin\tdescription', + }}) + + response = client.get('/rss') + + assert response.status_code == 200 + assert b'\x0b' not in response.data + assert b'\x0c' not in response.data + assert b'\x08' not in response.data + # the whole feed must still parse as XML + root = ElementTree.fromstring(response.data) + assert b'badname' in response.data + assert root.find('./channel/title').text == 'Latest activity - crackmes.one' + + def test_old_crackme_rss_url_is_kept_as_an_alias(client): assert client.get('/rss/crackme').status_code == 200