def test_submit_codename(source_app): """ Test preventions against people submitting their codename. """ with source_app.test_client() as app: InstanceConfig.get_default().update_submission_prefs( allow_uploads=True, min_length=0, reject_codenames=True) codename = new_codename(app, session) resp = app.post( url_for("main.submit"), data=dict(msg=codename, fh=(StringIO(""), "")), follow_redirects=True, ) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Please do not submit your codename!" in text # Do a dummy submission _dummy_submission(app) # Now resubmit the codename, should be accepted. resp = app.post( url_for("main.submit"), data=dict(msg=codename, fh=(StringIO(""), "")), follow_redirects=True, ) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Thanks! We received your message" in text
def test_submit_initial_short_message(source_app): """ Test the message size limit. """ with source_app.test_client() as app: InstanceConfig.get_default().update_submission_prefs( allow_uploads=True, min_length=10, reject_codenames=False) new_codename(app, session) resp = app.post( url_for("main.submit"), data=dict(msg="A" * 5, fh=(StringIO(""), "")), follow_redirects=True, ) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Your first message must be at least 10 characters long." in text # Now retry with a longer message resp = app.post( url_for("main.submit"), data=dict(msg="A" * 25, fh=(StringIO(""), "")), follow_redirects=True, ) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Thank you for sending this information to us." in text # Now send another short message, that should still be accepted since # it's no longer the initial one resp = app.post(url_for("main.submit"), data=dict(msg="A", fh=(StringIO(""), "")), follow_redirects=True) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Thanks! We received your message." in text
def lookup(logged_in_source: SourceUser) -> str: replies = [] logged_in_source_in_db = logged_in_source.get_db_record() source_inbox = Reply.query.filter_by( source_id=logged_in_source_in_db.id, deleted_by_source=False ).all() first_submission = logged_in_source_in_db.interaction_count == 0 if first_submission: min_message_length = InstanceConfig.get_default().initial_message_min_len else: min_message_length = 0 for reply in source_inbox: reply_path = Storage.get_default().path( logged_in_source.filesystem_id, reply.filename, ) try: with io.open(reply_path, "rb") as f: contents = f.read() decrypted_reply = EncryptionManager.get_default().decrypt_journalist_reply( for_source_user=logged_in_source, ciphertext_in=contents ) reply.decrypted = decrypted_reply except UnicodeDecodeError: current_app.logger.error("Could not decode reply %s" % reply.filename) except FileNotFoundError: current_app.logger.error("Reply file missing: %s" % reply.filename) else: reply.date = datetime.utcfromtimestamp(os.stat(reply_path).st_mtime) replies.append(reply) # Sort the replies by date replies.sort(key=operator.attrgetter("date"), reverse=True) # If not done yet, generate a keypair to encrypt replies from the journalist encryption_mgr = EncryptionManager.get_default() try: encryption_mgr.get_source_public_key(logged_in_source.filesystem_id) except GpgKeyNotFoundError: encryption_mgr.generate_source_key_pair(logged_in_source) return render_template( "lookup.html", is_user_logged_in=True, allow_document_uploads=InstanceConfig.get_default().allow_document_uploads, replies=replies, min_len=min_message_length, new_user_codename=session.get("new_user_codename", None), form=SubmissionForm(), )
def manage_config() -> Union[str, werkzeug.Response]: if InstanceConfig.get_default().initial_message_min_len > 0: prevent_short_messages = True else: prevent_short_messages = False # The UI document upload prompt ("prevent") is the opposite of the setting ("allow") submission_preferences_form = SubmissionPreferencesForm( prevent_document_uploads=not InstanceConfig.get_default( ).allow_document_uploads, prevent_short_messages=prevent_short_messages, min_message_length=InstanceConfig.get_default( ).initial_message_min_len, reject_codename_messages=InstanceConfig.get_default(). reject_message_with_codename, ) organization_name_form = OrgNameForm( organization_name=InstanceConfig.get_default().organization_name) logo_form = LogoForm() if logo_form.validate_on_submit(): f = logo_form.logo.data if current_app.static_folder is None: abort(500) custom_logo_filepath = os.path.join(current_app.static_folder, "i", "custom_logo.png") try: f.save(custom_logo_filepath) flash(gettext("Image updated."), "logo-success") except Exception: flash( # Translators: This error is shown when an uploaded image cannot be used. gettext( "Unable to process the image file. Please try another one." ), "logo-error", ) finally: return redirect( url_for("admin.manage_config") + "#config-logoimage") else: for field, errors in list(logo_form.errors.items()): for error in errors: flash(error, "logo-error") return render_template( "config.html", submission_preferences_form=submission_preferences_form, organization_name_form=organization_name_form, max_len=Submission.MAX_MESSAGE_LEN, logo_form=logo_form, )
def metadata() -> flask.Response: meta = { "organization_name": InstanceConfig.get_default().organization_name, "allow_document_uploads": InstanceConfig.get_default().allow_document_uploads, "gpg_fpr": config.JOURNALIST_KEY, "sd_version": version.__version__, "server_os": server_os.get_os_release(), "supported_languages": config.SUPPORTED_LOCALES, "v3_source_url": get_sourcev3_url(), } resp = make_response(json.dumps(meta)) resp.headers["Content-Type"] = "application/json" return resp
def setup_g() -> "Optional[Response]": """Store commonly used values in Flask's special g object""" if "expires" in session and datetime.now(timezone.utc) >= session["expires"]: session.clear() flash(gettext("You have been logged out due to inactivity."), "error") uid = session.get("uid", None) if uid: user = Journalist.query.get(uid) if user and "nonce" in session and session["nonce"] != user.session_nonce: session.clear() flash(gettext("You have been logged out due to password change"), "error") session["expires"] = datetime.now(timezone.utc) + timedelta( minutes=getattr(config, "SESSION_EXPIRATION_MINUTES", 120) ) uid = session.get("uid", None) if uid: g.user = Journalist.query.get(uid) # pylint: disable=assigning-non-slot i18n.set_locale(config) if InstanceConfig.get_default().organization_name: g.organization_name = ( # pylint: disable=assigning-non-slot InstanceConfig.get_default().organization_name ) else: g.organization_name = gettext("SecureDrop") # pylint: disable=assigning-non-slot try: g.logo = get_logo_url(app) # pylint: disable=assigning-non-slot except FileNotFoundError: app.logger.error("Site logo not found.") if request.path.split("/")[1] == "api": pass # We use the @token_required decorator for the API endpoints else: # We are not using the API if request.endpoint not in _insecure_views and not logged_in(): return redirect(url_for("main.login")) if request.method == "POST": filesystem_id = request.form.get("filesystem_id") if filesystem_id: g.filesystem_id = filesystem_id # pylint: disable=assigning-non-slot g.source = get_source(filesystem_id) # pylint: disable=assigning-non-slot return None
def validate_msg(self, field: wtforms.Field) -> None: if len(field.data) > Submission.MAX_MESSAGE_LEN: message = gettext("Message text too long.") if InstanceConfig.get_default().allow_document_uploads: message = "{} {}".format( message, gettext( "Large blocks of text must be uploaded as a file, not copied and pasted." ), ) raise ValidationError(message)
def test_submit_codename_second_login(source_app): """ Test codename submissions *not* prevented on second session """ with source_app.test_client() as app: InstanceConfig.get_default().update_submission_prefs( allow_uploads=True, min_length=0, reject_codenames=True) codename = new_codename(app, session) resp = app.post( url_for("main.submit"), data=dict(msg=codename, fh=(StringIO(""), "")), follow_redirects=True, ) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Please do not submit your codename!" in text resp = app.get(url_for("main.logout"), follow_redirects=True) assert not SessionManager.is_user_logged_in(db_session=db.session) text = resp.data.decode("utf-8") assert "This will clear your Tor Browser activity data" in text resp = app.post(url_for("main.login"), data=dict(codename=codename), follow_redirects=True) assert resp.status_code == 200 assert SessionManager.is_user_logged_in(db_session=db.session) resp = app.post( url_for("main.submit"), data=dict(msg=codename, fh=(StringIO(""), "")), follow_redirects=True, ) assert resp.status_code == 200 text = resp.data.decode("utf-8") assert "Thank you for sending this information" in text
def update_instance_config() -> None: InstanceConfig.get_default(refresh=True)
def submit(logged_in_source: SourceUser) -> werkzeug.Response: allow_document_uploads = InstanceConfig.get_default().allow_document_uploads form = SubmissionForm() if not form.validate(): for field, errors in form.errors.items(): for error in errors: flash_msg("error", None, error) return redirect(url_for("main.lookup")) msg = request.form["msg"] fh = None if allow_document_uploads and "fh" in request.files: fh = request.files["fh"] # Don't submit anything if it was an "empty" submission. #878 if not (msg or fh): if allow_document_uploads: html_contents = gettext("You must enter a message or choose a file to submit.") else: html_contents = gettext("You must enter a message.") flash_msg("error", None, html_contents) return redirect(url_for("main.lookup")) fnames = [] logged_in_source_in_db = logged_in_source.get_db_record() first_submission = logged_in_source_in_db.interaction_count == 0 if first_submission: min_len = InstanceConfig.get_default().initial_message_min_len if (min_len > 0) and (msg and not fh) and (len(msg) < min_len): flash_msg( "error", None, gettext("Your first message must be at least {} characters long.").format( min_len ), ) return redirect(url_for("main.lookup")) # if the new_user_codename key is not present in the session, this is # not a first session new_codename = session.get("new_user_codename", None) codenames_rejected = InstanceConfig.get_default().reject_message_with_codename if new_codename is not None: if codenames_rejected and codename_detected(msg, new_codename): flash_msg( "error", None, gettext("Please do not submit your codename!"), gettext( "Keep your codename secret, and use it to log in later to " "check for replies." ), ) return redirect(url_for("main.lookup")) if not os.path.exists(Storage.get_default().path(logged_in_source.filesystem_id)): current_app.logger.debug( "Store directory not found for source '{}', creating one.".format( logged_in_source_in_db.journalist_designation ) ) os.mkdir(Storage.get_default().path(logged_in_source.filesystem_id)) if msg: logged_in_source_in_db.interaction_count += 1 fnames.append( Storage.get_default().save_message_submission( logged_in_source_in_db.filesystem_id, logged_in_source_in_db.interaction_count, logged_in_source_in_db.journalist_filename, msg, ) ) if fh: logged_in_source_in_db.interaction_count += 1 fnames.append( Storage.get_default().save_file_submission( logged_in_source_in_db.filesystem_id, logged_in_source_in_db.interaction_count, logged_in_source_in_db.journalist_filename, fh.filename, fh.stream, ) ) if first_submission or msg or fh: if first_submission: html_contents = gettext( "Thank you for sending this information to us. Please " "check back later for replies." ) elif msg and not fh: html_contents = gettext("Thanks! We received your message.") elif fh and not msg: html_contents = gettext("Thanks! We received your document.") else: html_contents = gettext("Thanks! We received your message and document.") flash_msg("success", gettext("Success!"), html_contents) new_submissions = [] for fname in fnames: submission = Submission(logged_in_source_in_db, fname, Storage.get_default()) db.session.add(submission) new_submissions.append(submission) logged_in_source_in_db.pending = False logged_in_source_in_db.last_updated = datetime.now(timezone.utc) db.session.commit() for sub in new_submissions: store.async_add_checksum_for_file(sub, Storage.get_default()) normalize_timestamps(logged_in_source) return redirect(url_for("main.lookup"))