Repository: freedomofpress/securedrop

Issue:
Journalist API returns error 500 when JSON in malformed or absent
## Description

## Steps to Reproduce

1. `make dev`
2. `curl -X POST 127.0.0.1:8081/api/v1/token`

## Expected Behavior

The server should return an error 400 stating that the request is invalid.

## Actual Behavior

The server returns an error 500 stating that there is a server error.



Discussion:

Resolving patch:
diff --git a/securedrop/journalist_app/api.py b/securedrop/journalist_app/api.py
--- a/securedrop/journalist_app/api.py
+++ b/securedrop/journalist_app/api.py
@@ -65,6 +65,21 @@ def get_endpoints():
                      'auth_token_url': '/api/v1/token'}
         return jsonify(endpoints), 200
 
+    # Before every post, we validate the payload before processing the request
+    @api.before_request
+    def validate_data():
+        if request.method == 'POST':
+            # flag and star can have empty payloads
+            if not request.data:
+                if ('flag' not in request.path and 'star' not in request.path):
+                    return abort(400, 'malformed request')
+            # other requests must have valid JSON payload
+            else:
+                try:
+                    json.loads(request.data)
+                except (ValueError):
+                    return abort(400, 'malformed request')
+
     @api.route('/token', methods=['POST'])
     def get_token():
         creds = json.loads(request.data)


Test patch:
diff --git a/securedrop/tests/test_journalist_api.py b/securedrop/tests/test_journalist_api.py
--- a/securedrop/tests/test_journalist_api.py
+++ b/securedrop/tests/test_journalist_api.py
@@ -228,7 +228,8 @@ def test_user_without_token_cannot_post_protected_endpoints(journalist_app,
     with journalist_app.test_client() as app:
         for protected_route in protected_routes:
             response = app.post(protected_route,
-                                headers=get_api_headers(''))
+                                headers=get_api_headers(''),
+                                data=json.dumps({'some': 'stuff'}))
             assert response.status_code == 403
 
 
@@ -594,3 +595,66 @@ def test_reply_with_valid_square_json_400(journalist_app, journalist_api_token,
 
         json_response = json.loads(response.data)
         assert json_response['message'] == 'reply not found in request body'
+
+
+def test_malformed_json_400(journalist_app, journalist_api_token, test_journo,
+                            test_source):
+
+    with journalist_app.app_context():
+        uuid = test_source['source'].uuid
+        protected_routes = [
+            url_for('api.get_token'),
+            url_for('api.post_reply', source_uuid=uuid),
+            url_for('api.add_star', source_uuid=uuid),
+            url_for('api.flag', source_uuid=uuid),
+        ]
+    with journalist_app.test_client() as app:
+        for protected_route in protected_routes:
+
+            response = app.post(protected_route,
+                                data="{this is invalid {json!",
+                                headers=get_api_headers(journalist_api_token))
+            observed_response = json.loads(response.data)
+
+            assert response.status_code == 400
+            assert observed_response['error'] == 'Bad Request'
+
+
+def test_empty_json_400(journalist_app, journalist_api_token, test_journo,
+                        test_source):
+
+    with journalist_app.app_context():
+        uuid = test_source['source'].uuid
+        protected_routes = [
+            url_for('api.get_token'),
+            url_for('api.post_reply', source_uuid=uuid),
+        ]
+    with journalist_app.test_client() as app:
+        for protected_route in protected_routes:
+
+            response = app.post(protected_route,
+                                data="",
+                                headers=get_api_headers(journalist_api_token))
+            observed_response = json.loads(response.data)
+
+            assert response.status_code == 400
+            assert observed_response['error'] == 'Bad Request'
+
+
+def test_empty_json_20X(journalist_app, journalist_api_token, test_journo,
+                        test_source):
+
+    with journalist_app.app_context():
+        uuid = test_source['source'].uuid
+        protected_routes = [
+            url_for('api.add_star', source_uuid=uuid),
+            url_for('api.flag', source_uuid=uuid),
+        ]
+    with journalist_app.test_client() as app:
+        for protected_route in protected_routes:
+
+            response = app.post(protected_route,
+                                data="",
+                                headers=get_api_headers(journalist_api_token))
+
+            assert response.status_code in (200, 201)