Repository: freedomofpress/securedrop

Issue:
Malformed tokens throw exceptions
## Description

A header of the form `Authorization: some-token-value-here` will cause an exception.

## Steps to Reproduce

```bash
http --json GET localhost:8081/api/v1/sources 'Authorization: eyJhbGciOiJIUzI1NiIsImV4cCI6MTU0ODExNzEzNSwiaWF0IjoxNTQ4MDg4MzM1fQ.eyJpZCI6MX0.VjCdcVOPjsHWGl79wZAz4vjHVRANmCs_g-G6uyiFSl8'
# -----------------------------------------------------------^
# note the missing token "token"
```

## Expected Behavior

HTTP 403

## Actual Behavior

```pytb
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_app/api.py", line 40, in decorated_function
    auth_token = auth_header.split(" ")[1]
IndexError: list index out of range
```

## Comments

Detupling / indexing should be wrapped in a `try`/`except`.


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
@@ -37,7 +37,10 @@ def decorated_function(*args, **kwargs):
             return abort(403, 'API token not found in Authorization header.')
 
         if auth_header:
-            auth_token = auth_header.split(" ")[1]
+            split = auth_header.split(" ")
+            if len(split) != 2 or split[0] != 'Token':
+                abort(403, 'Malformed authorization header.')
+            auth_token = split[1]
         else:
             auth_token = ''
         if not Journalist.validate_api_token_and_get_user(auth_token):


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
@@ -860,3 +860,24 @@ def test_api_does_not_set_cookie_headers(journalist_app, test_journo):
         assert 'Set-Cookie' not in observed_headers.keys()
         if 'Vary' in observed_headers.keys():
             assert 'Cookie' not in observed_headers['Vary']
+
+
+# regression test for #4053
+def test_malformed_auth_token(journalist_app, journalist_api_token):
+    with journalist_app.app_context():
+        # we know this endpoint requires an auth header
+        url = url_for('api.get_all_sources')
+
+    with journalist_app.test_client() as app:
+        # precondition to ensure token is even valid
+        resp = app.get(url, headers={'Authorization': 'Token {}'.format(journalist_api_token)})
+        assert resp.status_code == 200
+
+        resp = app.get(url, headers={'Authorization': 'not-token {}'.format(journalist_api_token)})
+        assert resp.status_code == 403
+
+        resp = app.get(url, headers={'Authorization': journalist_api_token})
+        assert resp.status_code == 403
+
+        resp = app.get(url, headers={'Authorization': 'too many {}'.format(journalist_api_token)})
+        assert resp.status_code == 403