From 26871eeaccfb0caefeb8f0542c9a2a014f65bb65 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Mon, 18 Jan 2021 10:01:47 +0000 Subject: [PATCH] Validate CAP against the spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This gives us some extra confidence that there aren’t any problems with the data we’re getting from the other service. It doesn’t address any specific problems we’ve seen, rather it seems like a sensible precaution to take. --- app/v2/broadcast/post_broadcast.py | 10 +- app/xml_schemas/CAP-v1.2.xsd | 218 ++++++++++++++++++ app/xml_schemas/__init__.py | 19 ++ requirements-app.txt | 1 + requirements.txt | 9 +- tests/app/v2/broadcast/test_post_broadcast.py | 22 ++ 6 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 app/xml_schemas/CAP-v1.2.xsd create mode 100644 app/xml_schemas/__init__.py diff --git a/app/v2/broadcast/post_broadcast.py b/app/v2/broadcast/post_broadcast.py index a1b483808..18fe37b64 100644 --- a/app/v2/broadcast/post_broadcast.py +++ b/app/v2/broadcast/post_broadcast.py @@ -8,6 +8,8 @@ from app.models import BROADCAST_TYPE, BroadcastMessage, BroadcastStatusType from app.schema_validation import validate from app.v2.broadcast import v2_broadcast_blueprint from app.v2.broadcast.broadcast_schemas import post_broadcast_schema +from app.v2.errors import BadRequestError +from app.xml_schemas import validate_xml @v2_broadcast_blueprint.route("", methods=['POST']) @@ -21,7 +23,13 @@ def create_broadcast(): if request.content_type == 'application/json': broadcast_json = request.get_json() elif request.content_type == 'application/cap+xml': - broadcast_json = cap_xml_to_dict(request.get_data(as_text=True)) + cap_xml = request.get_data(as_text=True) + if not validate_xml(cap_xml, 'CAP-v1.2.xsd'): + raise BadRequestError( + message=f'Request data is not valid CAP XML', + status_code=400, + ) + broadcast_json = cap_xml_to_dict(cap_xml) else: raise BadRequestError( message=f'Content type {request.content_type} not supported', diff --git a/app/xml_schemas/CAP-v1.2.xsd b/app/xml_schemas/CAP-v1.2.xsd new file mode 100644 index 000000000..ed97952be --- /dev/null +++ b/app/xml_schemas/CAP-v1.2.xsd @@ -0,0 +1,218 @@ + + + + + + CAP Alert Message (version 1.2) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/xml_schemas/__init__.py b/app/xml_schemas/__init__.py new file mode 100644 index 000000000..4d8d11ef0 --- /dev/null +++ b/app/xml_schemas/__init__.py @@ -0,0 +1,19 @@ +from lxml import etree +from pathlib import Path + + +def validate_xml(document, schema_file_name): + + path = Path(__file__).resolve().parent / schema_file_name + contents = path.read_text() + + schema_root = etree.XML(contents.encode('utf-8')) + schema = etree.XMLSchema(schema_root) + parser = etree.XMLParser(schema=schema) + + try: + etree.fromstring(document, parser) + except etree.XMLSyntaxError: + return False + + return True diff --git a/requirements-app.txt b/requirements-app.txt index 8688b3922..cc017a729 100644 --- a/requirements-app.txt +++ b/requirements-app.txt @@ -24,6 +24,7 @@ strict-rfc3339==0.7 rfc3987==1.3.8 cachetools==4.2.0 beautifulsoup4==4.9.3 +lxml==4.6.2 notifications-python-client==5.7.1 diff --git a/requirements.txt b/requirements.txt index bb5c70107..f9390d13e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ strict-rfc3339==0.7 rfc3987==1.3.8 cachetools==4.2.0 beautifulsoup4==4.9.3 +lxml==4.6.2 notifications-python-client==5.7.1 @@ -39,18 +40,18 @@ prometheus-client==0.9.0 gds-metrics==0.2.4 ## The following requirements were added by pip freeze: -alembic==1.4.3 +alembic==1.5.0 amqp==1.4.9 anyjson==0.3.3 attrs==20.3.0 -awscli==1.18.215 +awscli==1.18.216 bcrypt==3.2.0 billiard==3.3.0.23 bleach==3.2.1 blinker==1.4 boto==2.49.0 -boto3==1.16.55 -botocore==1.19.55 +boto3==1.16.56 +botocore==1.19.56 certifi==2020.12.5 chardet==4.0.0 click==7.1.2 diff --git a/tests/app/v2/broadcast/test_post_broadcast.py b/tests/app/v2/broadcast/test_post_broadcast.py index 587fe489b..e38918add 100644 --- a/tests/app/v2/broadcast/test_post_broadcast.py +++ b/tests/app/v2/broadcast/test_post_broadcast.py @@ -122,3 +122,25 @@ def test_valid_post_cap_xml_broadcast_returns_201( assert response_json['template_name'] is None assert response_json['template_version'] is None assert response_json['updated_at'] is None + + +def test_invalid_post_cap_xml_broadcast_returns_400( + client, + sample_broadcast_service, +): + auth_header = create_authorization_header(service_id=sample_broadcast_service.id) + + response = client.post( + path='/v2/broadcast', + data="Oh no", + headers=[('Content-Type', 'application/cap+xml'), auth_header], + ) + + assert response.status_code == 400 + assert json.loads(response.get_data(as_text=True)) == { + 'errors': [{ + 'error': 'BadRequestError', + 'message': 'Request data is not valid CAP XML' + }], + 'status_code': 400, + }