Repository: freedomofpress/securedrop

Issue:
Expired Source Interface session causes server error on new session
## Description

Creating a new codename after a session has expired causes a server error.

## Steps to Reproduce

1. Visit the codename generation page (`/generate`) but do not press 'Submit Documents'
2. Wait for the session to expire
3. Press 'Submit Documents'

Diff to force session expiry at 1 minute:

```diff
diff --git a/securedrop/source_app/__init__.py b/securedrop/source_app/__init__.py
index d2679b737..60df38ece 100644
--- a/securedrop/source_app/__init__.py
+++ b/securedrop/source_app/__init__.py
@@ -148,9 +148,7 @@ def create_app(config):
             flash(Markup(msg), "important")
 
         session['expires'] = datetime.utcnow() + \
-            timedelta(minutes=getattr(config,
-                                      'SESSION_EXPIRATION_MINUTES',
-                                      120))
+            timedelta(minutes=1)
 
         # ignore_static here because `crypto_util.hash_codename` is scrypt
         # (very time consuming), and we don't need to waste time running if
```

## Expected Behavior

A new codename is generated or an appropriate error message is shown.

## Actual Behavior

The Source Interface displays:

> Server error
> Sorry, the website encountered an error and was unable to complete your request.


Discussion:
See also the similar #4458 which may have the same root cause. 

I've only reproduced this once as I haven't mucked around with forcing session expiry yet.
After forcing session expiration to be one minute, I can't reproduce this - are there any additional STR? 
Thanks for the repro attempt, will try a clean repro as well and document exact steps here if successful.
One way to get the error appears to be to wait with attempting to create the codename _until_ the expiry of the session. 

I've updated the issue accordingly, and added the diff I used for forcing session expiry. Here's the traceback from the dev env:

```
KeyError

KeyError: 'codename'
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/erik/Code/securedrop/securedrop/source_app/main.py", line 56, in create

    session['codename'])

    File "/usr/local/lib/python2.7/dist-packages/werkzeug/local.py", line 377, in <lambda>

    __getitem__ = lambda x, i: x._get_current_object()[i]

    File "/usr/local/lib/python2.7/dist-packages/flask/sessions.py", line 83, in __getitem__

    return super(SecureCookieSession, self).__getitem__(key)

    KeyError: 'codename'
```
I can reproduce this behavior in `develop`, on the 0.13.0 release branch, and at commit c24f61f7408b7e8f412d14ea3eab441178171938 (prior to fix for #4361).

Resolving patch:
diff --git a/securedrop/source_app/__init__.py b/securedrop/source_app/__init__.py
--- a/securedrop/source_app/__init__.py
+++ b/securedrop/source_app/__init__.py
@@ -145,7 +145,9 @@ def setup_g():
             # clear the session after we render the message so it's localized
             session.clear()
 
+            # Redirect to index with flashed message
             flash(Markup(msg), "important")
+            return redirect(url_for('main.index'))
 
         session['expires'] = datetime.utcnow() + \
             timedelta(minutes=getattr(config,


Test patch:
diff --git a/securedrop/tests/test_source.py b/securedrop/tests/test_source.py
--- a/securedrop/tests/test_source.py
+++ b/securedrop/tests/test_source.py
@@ -3,6 +3,7 @@
 import re
 import subprocess
 import six
+import time
 
 from io import BytesIO
 from flask import session, escape, current_app, url_for, g
@@ -667,7 +668,33 @@ def test_source_session_expiration(config, source_app):
         # which is always present and 'csrf_token' which leaks no info)
         session.pop('expires', None)
         session.pop('csrf_token', None)
-        assert not session, session
+        assert not session
+
+        text = resp.data.decode('utf-8')
+        assert 'Your session timed out due to inactivity' in text
+
+
+def test_source_session_expiration_create(config, source_app):
+    with source_app.test_client() as app:
+
+        seconds_session_expire = 1
+        config.SESSION_EXPIRATION_MINUTES = seconds_session_expire / 60.
+
+        # Make codename, and then wait for session to expire.
+        resp = app.get(url_for('main.generate'))
+        assert resp.status_code == 200
+
+        time.sleep(seconds_session_expire + 0.1)
+
+        # Now when we click create, the session will have expired.
+        resp = app.post(url_for('main.create'), follow_redirects=True)
+
+        # check that the session was cleared (apart from 'expires'
+        # which is always present and 'csrf_token' which leaks no info)
+        session.pop('expires', None)
+        session.pop('csrf_token', None)
+        assert not session
+
         text = resp.data.decode('utf-8')
         assert 'Your session timed out due to inactivity' in text