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, + }