From 6f0eafb2233feacd26951551393c4f1d0b7204dc Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Tue, 28 May 2019 10:59:51 +0530 Subject: [PATCH] Fixed CSRF security vulnerability issue. per Alvin Lindstam. Fixes #4217 Initial patch by: Khushboo Vashi Modified by: Ashesh Vashi and Murtuza Zabuawala --- docs/en_US/release_notes_4_7.rst | 1 + web/config.py | 7 +- web/pgadmin/__init__.py | 14 +- web/pgadmin/browser/__init__.py | 39 ++---- web/pgadmin/browser/static/js/browser.js | 15 ++- web/pgadmin/browser/static/js/collection.js | 3 +- web/pgadmin/browser/static/js/preferences.js | 7 +- .../browser/templates/browser/index.html | 1 - .../browser/templates/browser/js/utils.js | 3 + .../browser/tests/test_change_password.py | 15 +-- .../tests/test_gravatar_image_display.py | 13 +- web/pgadmin/browser/tests/test_login.py | 34 +++-- .../browser/tests/test_reset_password.py | 12 +- web/pgadmin/browser/tests/utils.py | 7 +- web/pgadmin/misc/__init__.py | 2 + .../dependencies/static/js/dependencies.js | 7 +- .../misc/dependents/static/js/dependents.js | 7 +- .../misc/file_manager/static/js/utility.js | 6 +- web/pgadmin/misc/sql/static/js/sql.js | 5 +- .../misc/statistics/static/js/statistics.js | 9 +- .../setup/tests/test_export_import_servers.py | 13 +- web/pgadmin/static/js/csrf.js | 60 +++++++++ .../static/js/sqleditor/execute_query.js | 6 +- .../static/js/tree/pgadmin_tree_save_state.js | 2 +- .../tools/backup/static/js/backup_dialog.js | 3 +- .../backup/static/js/backup_dialog_wrapper.js | 3 +- .../tools/debugger/static/js/direct.js | 5 +- .../tools/restore/static/js/restore_dialog.js | 3 +- .../static/js/restore_dialog_wrapper.js | 3 +- .../tools/sqleditor/static/js/sqleditor.js | 6 +- web/pgadmin/tools/user_management/__init__.py | 2 + web/pgadmin/utils/csrf.py | 43 ++++++ web/pgadmin/utils/session.py | 2 +- .../python_test_utils/csrf_test_client.py | 124 ++++++++++++++++++ .../python_test_utils/test_utils.py | 18 +-- web/regression/runtests.py | 11 +- 36 files changed, 387 insertions(+), 124 deletions(-) create mode 100644 web/pgadmin/static/js/csrf.js create mode 100644 web/pgadmin/utils/csrf.py create mode 100644 web/regression/python_test_utils/csrf_test_client.py diff --git a/docs/en_US/release_notes_4_7.rst b/docs/en_US/release_notes_4_7.rst index d1d96ff8f..935640212 100644 --- a/docs/en_US/release_notes_4_7.rst +++ b/docs/en_US/release_notes_4_7.rst @@ -15,6 +15,7 @@ Bug fixes | `Bug #4164 `_ - Fix file browser path issue which occurs when client is on Windows and server is on Mac/Linux. | `Bug #4194 `_ - Fix accessibility issue for menu navigation. | `Bug #4208 `_ - Update the UI logo. +| `Bug #4217 `_ - Fixed CSRF security vulnerability issue. | `Bug #4218 `_ - Properly assign dropdownParent in Select2 controls. | `Bug #4219 `_ - Ensure popper.js is installed when needed. | `Bug #4227 `_ - Fixed Tab key navigation for Maintenance dialog. diff --git a/web/config.py b/web/config.py index 785f07481..0d385a398 100644 --- a/web/config.py +++ b/web/config.py @@ -123,6 +123,10 @@ if (not hasattr(builtins, 'SERVER_MODE')) or builtins.SERVER_MODE is None: else: SERVER_MODE = builtins.SERVER_MODE +# HTTP headers to search for CSRF token when it is not provided in the form. +# Default is ['X-CSRFToken', 'X-CSRF-Token'] +WTF_CSRF_HEADERS = ['X-pgA-CSRFToken'] + # User ID (email address) to use for the default user in desktop mode. # The default should be fine here, as it's not exposed in the app. DESKTOP_USER = 'pgadmin4@pgadmin.org' @@ -141,9 +145,6 @@ DEFAULT_SERVER = '127.0.0.1' # environment by the runtime DEFAULT_SERVER_PORT = 5050 -# Enable CSRF protection? -CSRF_ENABLED = True - # Enable X-Frame-Option protection. # Set to one of "SAMEORIGIN", "ALLOW-FROM origin" or "" to disable. # Note that "DENY" is NOT supported (and will be silently ignored). diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 9a78e8987..19b47f54d 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -24,6 +24,7 @@ from flask_mail import Mail from flask_paranoid import Paranoid from flask_security import Security, SQLAlchemyUserDatastore, current_user from flask_security.utils import login_user + from werkzeug.datastructures import ImmutableDict from werkzeug.local import LocalProxy from werkzeug.utils import find_modules @@ -37,6 +38,7 @@ from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader from datetime import timedelta from pgadmin.setup import get_version, set_version from pgadmin.utils.ajax import internal_server_error +from pgadmin.utils.csrf import pgCSRFProtect # If script is running under python3, it will not have the xrange function @@ -367,7 +369,10 @@ def create_app(app_name=None): 'CSRF_SESSION_KEY': config.CSRF_SESSION_KEY, 'SECRET_KEY': config.SECRET_KEY, 'SECURITY_PASSWORD_SALT': config.SECURITY_PASSWORD_SALT, - 'SESSION_COOKIE_DOMAIN': config.SESSION_COOKIE_DOMAIN + 'SESSION_COOKIE_DOMAIN': config.SESSION_COOKIE_DOMAIN, + # CSRF Token expiration till session expires + 'WTF_CSRF_TIME_LIMIT': getattr(config, 'CSRF_TIME_LIMIT', None), + 'WTF_CSRF_METHODS': ['GET', 'POST', 'PUT', 'DELETE'], })) security.init_app(app, user_datastore) @@ -706,8 +711,13 @@ def create_app(app_name=None): current_app.logger.error(e, exc_info=True) return e + ########################################################################## + # Protection against CSRF attacks + ########################################################################## + with app.app_context(): + pgCSRFProtect.init_app(app) + ########################################################################## # All done! ########################################################################## - return app diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index fe2af7dc9..cd7788592 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -37,6 +37,7 @@ from pgadmin import current_blueprint from pgadmin.settings import get_setting from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response +from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.preferences import Preferences from pgadmin.browser.register_browser_preferences import \ register_browser_preferences @@ -478,6 +479,7 @@ class BrowserPluginModule(PgAdminModule): @blueprint.route("/") +@pgCSRFProtect.exempt @login_required def index(): """Render and process the main browser window.""" @@ -561,6 +563,7 @@ def index(): @blueprint.route("/js/utils.js") +@pgCSRFProtect.exempt @login_required def utils(): layout = get_setting('Browser/Layout', default='') @@ -627,6 +630,7 @@ def utils(): @blueprint.route("/js/endpoints.js") +@pgCSRFProtect.exempt def exposed_urls(): return make_response( render_template('browser/js/endpoints.js'), @@ -635,6 +639,7 @@ def exposed_urls(): @blueprint.route("/js/error.js") +@pgCSRFProtect.exempt @login_required def error_js(): return make_response( @@ -642,42 +647,16 @@ def error_js(): 200, {'Content-Type': 'application/javascript'}) -@blueprint.route("/js/node.js") -@login_required -def node_js(): - prefs = Preferences.module('paths') - - pg_help_path_pref = prefs.preference('pg_help_path') - pg_help_path = pg_help_path_pref.get() - - edbas_help_path_pref = prefs.preference('edbas_help_path') - edbas_help_path = edbas_help_path_pref.get() - - return make_response( - render_template('browser/js/node.js', - pg_help_path=pg_help_path, - edbas_help_path=edbas_help_path, - _=gettext - ), - 200, {'Content-Type': 'application/javascript'}) - - @blueprint.route("/js/messages.js") +@pgCSRFProtect.exempt def messages_js(): return make_response( render_template('browser/js/messages.js', _=gettext), 200, {'Content-Type': 'application/javascript'}) -@blueprint.route("/js/collection.js") -@login_required -def collection_js(): - return make_response( - render_template('browser/js/collection.js', _=gettext), - 200, {'Content-Type': 'application/javascript'}) - - @blueprint.route("/browser.css") +@pgCSRFProtect.exempt @login_required def browser_css(): """Render and return CSS snippets from the nodes and modules.""" @@ -711,6 +690,7 @@ def get_nodes(): if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE: @blueprint.route("/change_password", endpoint="change_password", methods=['GET', 'POST']) + @pgCSRFProtect.exempt @login_required def change_password(): """View function which handles a change password request.""" @@ -794,6 +774,7 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: @blueprint.route("/reset_password", endpoint="forgot_password", methods=['GET', 'POST']) + @pgCSRFProtect.exempt @anonymous_user_required def forgot_password(): """View function that handles a forgotten password request.""" @@ -860,10 +841,10 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: methods=['GET', 'POST'], endpoint='reset_password' ) + @pgCSRFProtect.exempt @anonymous_user_required def reset_password(token): """View function that handles a reset password request.""" - expired, invalid, user = reset_password_token_status(token) if invalid: diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 66c3d33da..9ff36087e 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -11,7 +11,8 @@ define('pgadmin.browser', [ 'sources/tree/tree', 'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore', 'underscore.string', 'bootstrap', 'sources/pgadmin', 'pgadmin.alertifyjs', 'bundled_codemirror', - 'sources/check_node_visibility', './toolbar', 'pgadmin.help', 'pgadmin.browser.utils', + 'sources/check_node_visibility', './toolbar', 'pgadmin.help', + 'sources/csrf', 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'jquery.aciplugin', 'jquery.acitree', 'pgadmin.browser.preferences', 'pgadmin.browser.messages', 'pgadmin.browser.menu', 'pgadmin.browser.panel', @@ -23,7 +24,7 @@ define('pgadmin.browser', [ tree, gettext, url_for, require, $, _, S, Bootstrap, pgAdmin, Alertify, codemirror, - checkNodeVisibility, toolBar, help + checkNodeVisibility, toolBar, help, csrfToken ) { window.jQuery = window.$ = $; // Some scripts do export their object in the window only. @@ -36,6 +37,8 @@ define('pgadmin.browser', [ var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {}; var select_object_msg = gettext('Please select an object in the tree view.'); + csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); + var panelEvents = {}; panelEvents[wcDocker.EVENT.VISIBILITY_CHANGED] = function() { if (this.isVisible()) { @@ -353,8 +356,8 @@ define('pgadmin.browser', [ }, save_current_layout: function(layout_id, docker) { if(docker) { - var layout = docker.save(); - var settings = { setting: layout_id, value: layout }; + var layout = docker.save(), + settings = { setting: layout_id, value: layout }; $.ajax({ type: 'POST', url: url_for('settings.store_bulk'), @@ -525,11 +528,15 @@ define('pgadmin.browser', [ pgBrowser.utils.registerScripts(this); pgBrowser.utils.addMenus(obj); + let headers = {}; + headers[pgAdmin.csrf_token_header] = pgAdmin.csrf_token; + // Ping the server every 5 minutes setInterval(function() { $.ajax({ url: url_for('misc.cleanup'), type:'POST', + headers: headers, }) .done(function() {}) .fail(function() {}); diff --git a/web/pgadmin/browser/static/js/collection.js b/web/pgadmin/browser/static/js/collection.js index 0549ebc98..57ae311a3 100644 --- a/web/pgadmin/browser/static/js/collection.js +++ b/web/pgadmin/browser/static/js/collection.js @@ -267,7 +267,8 @@ define([ $.ajax({ url: urlBase, type: 'GET', - beforeSend: function() { + beforeSend: function(xhr) { + xhr.setRequestHeader(pgAdmin.csrf_token_header, pgAdmin.csrf_token); // Generate a timer for the request timer = setTimeout(function() { // notify user if request is taking longer than 1 second diff --git a/web/pgadmin/browser/static/js/preferences.js b/web/pgadmin/browser/static/js/preferences.js index 96330aff3..680d18f49 100644 --- a/web/pgadmin/browser/static/js/preferences.js +++ b/web/pgadmin/browser/static/js/preferences.js @@ -12,6 +12,7 @@ import url_for from 'sources/url_for'; import $ from 'jquery'; import * as Alertify from 'pgadmin.alertifyjs'; import * as SqlEditorUtils from 'sources/sqleditor_utils'; + var modifyAnimation = require('sources/modify_animation'); const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {}; @@ -88,10 +89,14 @@ _.extend(pgBrowser, { // Get and cache the preferences cache_preferences: function (modulesChanged) { - var self = this; + var self = this, + headers = {}; + headers[pgAdmin.csrf_token_header] = pgAdmin.csrf_token; + setTimeout(function() { $.ajax({ url: url_for('preferences.get_all'), + headers: headers, }) .done(function(res) { self.preferences_cache = res; diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index ee4af6909..0a785d6ba 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -22,7 +22,6 @@ console.log(arguments); /* Show proper error dialog */ console.log(err); } - /* * Show loading spinner till every js module is loaded completely * Referenced url: diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js index eac96b144..cefe9ae7e 100644 --- a/web/pgadmin/browser/templates/browser/js/utils.js +++ b/web/pgadmin/browser/templates/browser/js/utils.js @@ -7,6 +7,7 @@ // ////////////////////////////////////////////////////////////// + define('pgadmin.browser.utils', ['sources/pgadmin'], function(pgAdmin) { @@ -15,6 +16,8 @@ define('pgadmin.browser.utils', /* Add hooked-in panels by extensions */ pgBrowser['panels_items'] = '{{ current_app.panels|tojson }}'; + pgAdmin['csrf_token_header'] = '{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}'; + pgAdmin['csrf_token'] = '{{ csrf_token() }}'; // Define list of nodes on which Query tool option doesn't appears var unsupported_nodes = pgAdmin.unsupported_nodes = [ diff --git a/web/pgadmin/browser/tests/test_change_password.py b/web/pgadmin/browser/tests/test_change_password.py index ff71f0c8a..ab329dbe9 100644 --- a/web/pgadmin/browser/tests/test_change_password.py +++ b/web/pgadmin/browser/tests/test_change_password.py @@ -105,20 +105,13 @@ class ChangePasswordTestCase(BaseTestGenerator): ) user_id = json.loads(response.data.decode('utf-8'))['id'] # Logout the Administrator before login normal user - test_utils.logout_tester_account(self.tester) - response = self.tester.post( - '/login', - data=dict( - email=self.username, - password=self.password - ), - follow_redirects=True - ) + self.tester.logout() + response = self.tester.login(self.username, self.password, True) self.assertEquals(response.status_code, 200) # test the 'change password' test case utils.change_password(self) # Delete the normal user after changing it's password - test_utils.logout_tester_account(self.tester) + self.tester.logout() # Login the Administrator before deleting normal user test_utils.login_tester_account(self.tester) response = self.tester.delete( @@ -131,4 +124,6 @@ class ChangePasswordTestCase(BaseTestGenerator): @classmethod def tearDownClass(cls): + # Make sure - we're already logged out before running + cls.tester.logout() test_utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/browser/tests/test_gravatar_image_display.py b/web/pgadmin/browser/tests/test_gravatar_image_display.py index a8d1b1b70..b63549f2d 100644 --- a/web/pgadmin/browser/tests/test_gravatar_image_display.py +++ b/web/pgadmin/browser/tests/test_gravatar_image_display.py @@ -41,7 +41,7 @@ class TestLoginUserImage(BaseTestGenerator): @classmethod def setUpClass(cls): "Logout first if already logged in" - utils.logout_tester_account(cls.tester) + cls.tester.logout() # No need to call baseclass setup function def setUp(self): @@ -49,13 +49,8 @@ class TestLoginUserImage(BaseTestGenerator): def runTest(self): # Login and check type of image in response - response = self.tester.post( - '/login', data=dict( - email=self.email, - password=self.password - ), - follow_redirects=True - ) + response = self.tester.login(self.email, self.password, True) + # Should have gravatar image if config.SHOW_GRAVATAR_IMAGE: self.assertIn(self.respdata, response.data.decode('utf8')) @@ -69,4 +64,6 @@ class TestLoginUserImage(BaseTestGenerator): We need to again login the test client as soon as test scenarios finishes. """ + # Make sure - we're already logged out + cls.tester.logout() utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/browser/tests/test_login.py b/web/pgadmin/browser/tests/test_login.py index 89d791112..fad2dd56b 100644 --- a/web/pgadmin/browser/tests/test_login.py +++ b/web/pgadmin/browser/tests/test_login.py @@ -8,7 +8,7 @@ ########################################################################## import uuid - +import config as app_config from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils import test_utils as utils from regression.test_setup import config_data @@ -28,6 +28,7 @@ class LoginTestCase(BaseTestGenerator): config_data['pgAdmin4_login_credentials'] ['login_username']), password=str(uuid.uuid4())[4:8], + is_gravtar_image_check=False, respdata='Invalid password')), # This test case validates the empty password field @@ -35,6 +36,7 @@ class LoginTestCase(BaseTestGenerator): email=( config_data['pgAdmin4_login_credentials'] ['login_username']), password='', + is_gravtar_image_check=False, respdata='Password not provided')), # This test case validates blank email field @@ -42,11 +44,13 @@ class LoginTestCase(BaseTestGenerator): email='', password=( config_data['pgAdmin4_login_credentials'] ['login_password']), + is_gravtar_image_check=False, respdata='Email not provided')), # This test case validates empty email and password ('Empty_Credentials', dict( email='', password='', + is_gravtar_image_check=False, respdata='Email not provided')), # This test case validates the invalid/incorrect email id @@ -55,12 +59,14 @@ class LoginTestCase(BaseTestGenerator): password=( config_data['pgAdmin4_login_credentials'] ['login_password']), + is_gravtar_image_check=False, respdata='Specified user does not exist')), # This test case validates invalid email and password ('Invalid_Credentials', dict( email=str(uuid.uuid4())[1:8] + '@xyz.com', password=str(uuid.uuid4())[4:8], + is_gravtar_image_check=False, respdata='Specified user does not exist')), # This test case validates the valid/correct credentials and allow user @@ -72,9 +78,13 @@ class LoginTestCase(BaseTestGenerator): password=( config_data['pgAdmin4_login_credentials'] ['login_password']), + is_gravtar_image_check=True, + respdata_without_gravtar=config_data['pgAdmin4_login_credentials'] + ['login_username'], respdata='Gravatar image for %s' % config_data['pgAdmin4_login_credentials'] - ['login_username'])) + ['login_username']), + ) ] @classmethod @@ -84,7 +94,7 @@ class LoginTestCase(BaseTestGenerator): logging in the client like invalid password, invalid emails, empty credentials etc. """ - utils.logout_tester_account(cls.tester) + cls.tester.logout() # No need to call base class setup function def setUp(self): @@ -92,15 +102,14 @@ class LoginTestCase(BaseTestGenerator): def runTest(self): """This function checks login functionality.""" - response = self.tester.post( - '/login', - data=dict( - email=self.email, - password=self.password - ), - follow_redirects=True - ) - self.assertTrue(self.respdata in response.data.decode('utf8')) + res = self.tester.login(self.email, self.password, True) + if self.is_gravtar_image_check: + if app_config.SHOW_GRAVATAR_IMAGE: + self.assertTrue(self.respdata in res.data.decode('utf8')) + else: + print(self.respdata_without_gravtar in res.data.decode('utf8')) + else: + self.assertTrue(self.respdata in res.data.decode('utf8')) @classmethod def tearDownClass(cls): @@ -108,4 +117,5 @@ class LoginTestCase(BaseTestGenerator): We need to again login the test client as soon as test scenarios finishes. """ + cls.tester.logout() utils.login_tester_account(cls.tester) diff --git a/web/pgadmin/browser/tests/test_reset_password.py b/web/pgadmin/browser/tests/test_reset_password.py index d1327b8e5..4fd1fecf6 100644 --- a/web/pgadmin/browser/tests/test_reset_password.py +++ b/web/pgadmin/browser/tests/test_reset_password.py @@ -11,7 +11,6 @@ import uuid from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils.test_utils import login_tester_account -from regression.python_test_utils.test_utils import logout_tester_account from regression.test_setup import config_data @@ -40,7 +39,7 @@ class ResetPasswordTestCase(BaseTestGenerator): @classmethod def setUpClass(cls): - logout_tester_account(cls.tester) + cls.tester.logout() # No need to call baseclass setup function def setUp(self): @@ -50,8 +49,13 @@ class ResetPasswordTestCase(BaseTestGenerator): """This function checks reset password functionality.""" response = self.tester.get('/browser/reset_password') - self.assertTrue('Recover pgAdmin 4 Password' in response.data.decode( - 'utf-8')) + self.assertTrue( + 'Recover Password' in response.data.decode('utf-8') + ) + self.assertTrue( + 'Enter the email address for the user account you wish to ' + 'recover the password for' in response.data.decode('utf-8') + ) response = self.tester.post( '/browser/reset_password', data=dict(email=self.email), follow_redirects=True) diff --git a/web/pgadmin/browser/tests/utils.py b/web/pgadmin/browser/tests/utils.py index 9e4905d39..895db991a 100644 --- a/web/pgadmin/browser/tests/utils.py +++ b/web/pgadmin/browser/tests/utils.py @@ -13,15 +13,18 @@ def change_password(self): '/browser/change_password', follow_redirects=True ) self.assertTrue( - 'pgAdmin 4 Password Change' in response.data.decode('utf-8') + 'Password Change' in response.data.decode('utf-8') ) + csrf_token = self.tester.fetch_csrf(response) + response = self.tester.post( '/browser/change_password', data=dict( password=self.password, new_password=self.new_password, - new_password_confirm=self.new_password_confirm + new_password_confirm=self.new_password_confirm, + csrf_token=csrf_token, ), follow_redirects=True ) diff --git a/web/pgadmin/misc/__init__.py b/web/pgadmin/misc/__init__.py index d50f04686..3c79d4d7d 100644 --- a/web/pgadmin/misc/__init__.py +++ b/web/pgadmin/misc/__init__.py @@ -13,6 +13,7 @@ import pgadmin.utils.driver as driver from flask import url_for, render_template, Response, request from flask_babelex import gettext from pgadmin.utils import PgAdminModule +from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.preferences import Preferences from pgadmin.utils.session import cleanup_session_files @@ -98,6 +99,7 @@ def ping(): # For Garbage Collecting closed connections @blueprint.route("/cleanup", methods=['POST']) +@pgCSRFProtect.exempt def cleanup(): driver.ping() # Cleanup session files. diff --git a/web/pgadmin/misc/dependencies/static/js/dependencies.js b/web/pgadmin/misc/dependencies/static/js/dependencies.js index 18fb37b39..e8bddf7aa 100644 --- a/web/pgadmin/misc/dependencies/static/js/dependencies.js +++ b/web/pgadmin/misc/dependencies/static/js/dependencies.js @@ -9,8 +9,8 @@ define('misc.dependencies', [ 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', - 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', -], function(gettext, _, S, $, Backbone, pgBrowser, Alertify, Backgrid) { + 'pgadmin', 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', +], function(gettext, _, S, $, Backbone, pgAdmin, pgBrowser, Alertify, Backgrid) { if (pgBrowser.NodeDependencies) return pgBrowser.NodeDependencies; @@ -150,7 +150,8 @@ define('misc.dependencies', [ $.ajax({ url: url, type: 'GET', - beforeSend: function() { + beforeSend: function(xhr) { + xhr.setRequestHeader(pgAdmin.csrf_token_header, pgAdmin.csrf_token); // Generate a timer for the request timer = setTimeout(function() { // notify user if request is taking longer than 1 second diff --git a/web/pgadmin/misc/dependents/static/js/dependents.js b/web/pgadmin/misc/dependents/static/js/dependents.js index 1c3db62f0..1547556e8 100644 --- a/web/pgadmin/misc/dependents/static/js/dependents.js +++ b/web/pgadmin/misc/dependents/static/js/dependents.js @@ -9,8 +9,8 @@ define('misc.dependents', [ 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', - 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', -], function(gettext, _, S, $, Backbone, pgBrowser, Alertify, Backgrid) { + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.alertifyjs', 'pgadmin.backgrid', +], function(gettext, _, S, $, Backbone, pgAdmin, pgBrowser, Alertify, Backgrid) { if (pgBrowser.NodeDependents) return pgBrowser.NodeDependents; @@ -156,7 +156,8 @@ define('misc.dependents', [ $.ajax({ url: url, type: 'GET', - beforeSend: function() { + beforeSend: function(xhr) { + xhr.setRequestHeader(pgAdmin.csrf_token_header, pgAdmin.csrf_token); // Generate a timer for the request timer = setTimeout(function() { // notify user if request is taking longer than 1 second diff --git a/web/pgadmin/misc/file_manager/static/js/utility.js b/web/pgadmin/misc/file_manager/static/js/utility.js index d1858ce74..0d83058de 100644 --- a/web/pgadmin/misc/file_manager/static/js/utility.js +++ b/web/pgadmin/misc/file_manager/static/js/utility.js @@ -22,12 +22,14 @@ import loading_icon from 'acitree/image/load-root.gif'; define([ 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs', 'sources/gettext', 'sources/url_for', 'dropzone', 'sources/pgadmin', - 'tablesorter', -], function($, _, S, Alertify, gettext, url_for, Dropzone, pgAdmin) { + 'sources/csrf', 'tablesorter', +], function($, _, S, Alertify, gettext, url_for, Dropzone, pgAdmin, csrfToken) { /*--------------------------------------------------------- Define functions used for various operations ---------------------------------------------------------*/ + // Set the CSRF Token + csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); // Return file extension var getFileExtension = function(name) { diff --git a/web/pgadmin/misc/sql/static/js/sql.js b/web/pgadmin/misc/sql/static/js/sql.js index 20f0af1e7..212838d0c 100644 --- a/web/pgadmin/misc/sql/static/js/sql.js +++ b/web/pgadmin/misc/sql/static/js/sql.js @@ -123,7 +123,10 @@ define('misc.sql', [ $.ajax({ url: url, type: 'GET', - beforeSend: function() { + beforeSend: function(xhr) { + xhr.setRequestHeader( + pgAdmin.csrf_token_header, pgAdmin.csrf_token + ); // Generate a timer for the request timer = setTimeout(function() { // Notify user if request is taking longer than 1 second diff --git a/web/pgadmin/misc/statistics/static/js/statistics.js b/web/pgadmin/misc/statistics/static/js/statistics.js index 77edc18b8..f6559966a 100644 --- a/web/pgadmin/misc/statistics/static/js/statistics.js +++ b/web/pgadmin/misc/statistics/static/js/statistics.js @@ -9,10 +9,10 @@ define('misc.statistics', [ 'sources/gettext', 'underscore', 'underscore.string', 'jquery', 'backbone', - 'pgadmin.browser', 'pgadmin.backgrid', 'alertify', 'sources/size_prettify', + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backgrid', 'alertify', 'sources/size_prettify', 'sources/misc/statistics/statistics', ], function( - gettext, _, S, $, Backbone, pgBrowser, Backgrid, Alertify, sizePrettify, + gettext, _, S, $, Backbone, pgAdmin, pgBrowser, Backgrid, Alertify, sizePrettify, statisticsHelper ) { @@ -235,7 +235,10 @@ define('misc.statistics', [ $.ajax({ url: url, type: 'GET', - beforeSend: function() { + beforeSend: function(xhr) { + xhr.setRequestHeader( + pgAdmin.csrf_token_header, pgAdmin.csrf_token + ); // Generate a timer for the request timer = setTimeout(function() { // notify user if request is taking longer than 1 second diff --git a/web/pgadmin/setup/tests/test_export_import_servers.py b/web/pgadmin/setup/tests/test_export_import_servers.py index 72162906c..564384adb 100644 --- a/web/pgadmin/setup/tests/test_export_import_servers.py +++ b/web/pgadmin/setup/tests/test_export_import_servers.py @@ -11,6 +11,7 @@ from pgadmin.utils.route import BaseTestGenerator import os import json import tempfile +import config class ImportExportServersTestCase(BaseTestGenerator): @@ -24,12 +25,20 @@ class ImportExportServersTestCase(BaseTestGenerator): ] def runTest(self): + + if config.SERVER_MODE is True: + self.skipTest( + "Can not run import-export of servers in the SERVER mode." + ) + path = os.path.dirname(__file__) setup = os.path.realpath(os.path.join(path, "../../../setup.py")) # Load the servers - os.system("python %s --load-servers %s 2> %s" % - (setup, os.path.join(path, "servers.json"), os.devnull)) + os.system( + "python %s --load-servers %s 2> %s" % + (setup, os.path.join(path, "servers.json"), os.devnull) + ) # And dump them again tf = tempfile.NamedTemporaryFile(delete=False) diff --git a/web/pgadmin/static/js/csrf.js b/web/pgadmin/static/js/csrf.js new file mode 100644 index 000000000..0c8dfcbed --- /dev/null +++ b/web/pgadmin/static/js/csrf.js @@ -0,0 +1,60 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2019, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import $ from 'jquery'; +import Backbone from 'backbone'; +import axios from 'axios'; + +export function setPGCSRFToken(header, token) { + if (!token) { + // Throw error message. + throw 'csrf-token meta tag has not been set'; + } + + // Configure Backbone.sync to set CSRF-Token-header request header for + // every requests except GET. + var origBackboneSync = Backbone.sync; + Backbone.sync = function(method, model, options) { + options.beforeSend = function(xhr) { + xhr.setRequestHeader(header, token); + }; + + return origBackboneSync(method, model, options); + }; + + // Configure Backbone.get to set 'X-CSRFToken' request header for + // GET requests. + var origBackboneGet = Backbone.get; + Backbone.get = function(method, model, options) { + options.beforeSend = function(xhr) { + xhr.setRequestHeader(header, token); + }; + + return origBackboneGet(method, model, options); + }; + + // Configure jquery.ajax to set 'X-CSRFToken' request header for + // every requests. + $.ajaxSetup({ + beforeSend: function(xhr) { + xhr.setRequestHeader(header, token); + }, + }); + + // Configure axios to set 'X-CSRFToken' request header for + // every requests. + axios.interceptors.request.use(function (config) { + config.headers[header] = token; + + return config; + }, function (error) { + return Promise.reject(error); + }); + +} diff --git a/web/pgadmin/static/js/sqleditor/execute_query.js b/web/pgadmin/static/js/sqleditor/execute_query.js index c3c9dc2af..82342c2b4 100644 --- a/web/pgadmin/static/js/sqleditor/execute_query.js +++ b/web/pgadmin/static/js/sqleditor/execute_query.js @@ -57,13 +57,12 @@ class ExecuteQuery { if (sqlStatement.length <= 0) return; const self = this; - let service = axios.create({}); self.explainPlan = explainPlan; const sqlStatementWithAnalyze = ExecuteQuery.prepareAnalyzeSql(sqlStatement, explainPlan); self.initializeExecutionOnSqlEditor(sqlStatementWithAnalyze); - service.post( + axios.post( this.generateURLReconnectionFlag(connect), JSON.stringify(sqlStatementWithAnalyze), {headers: {'Content-Type': 'application/json'}}) @@ -113,8 +112,7 @@ class ExecuteQuery { poll() { const self = this; - let service = axios.create({}); - service.get( + axios.get( url_for('sqleditor.poll', { 'trans_id': self.sqlServerObject.transId, }) diff --git a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js index 0d2071f02..d4340833a 100644 --- a/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js +++ b/web/pgadmin/static/js/tree/pgadmin_tree_save_state.js @@ -146,7 +146,7 @@ _.extend(pgBrowser.browserTreeState, { } } console.warn( - gettext('Error fetching the tree state."'), msg); + gettext('Error fetching the tree state.'), msg); }); }, update_cache: function(item) { diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog.js b/web/pgadmin/tools/backup/static/js/backup_dialog.js index 0c02a47c0..1aa545214 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog.js @@ -43,8 +43,7 @@ export class BackupDialog extends Dialog { const baseUrl = this.url_for_utility_exists(sid, params); // Check pg_dump or pg_dumpall utility exists or not. let that = this; - let service = axios.create({}); - service.get( + axios.get( baseUrl ).then(function(res) { if (!res.data.success) { diff --git a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js index 9dd99a13a..b2e9cb3d4 100644 --- a/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js +++ b/web/pgadmin/tools/backup/static/js/backup_dialog_wrapper.js @@ -142,8 +142,7 @@ export class BackupDialogWrapper extends DialogWrapper { this.setExtraParameters(selectedTreeNode, treeInfo); - let service = axios.create({}); - service.post( + axios.post( baseUrl, this.view.model.toJSON() ).then(function (res) { diff --git a/web/pgadmin/tools/debugger/static/js/direct.js b/web/pgadmin/tools/debugger/static/js/direct.js index d0bcf3ee6..23b61c4c4 100644 --- a/web/pgadmin/tools/debugger/static/js/direct.js +++ b/web/pgadmin/tools/debugger/static/js/direct.js @@ -353,7 +353,10 @@ define([ $.ajax({ url: baseUrl, method: 'GET', - beforeSend: function() { + beforeSend: function(xhr) { + xhr.setRequestHeader( + pgAdmin.csrf_token_header, pgAdmin.csrf_token + ); // set cursor to progress before every poll. $('.debugger-container').addClass('show_progress'); }, diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog.js b/web/pgadmin/tools/restore/static/js/restore_dialog.js index 019e20197..b73629665 100644 --- a/web/pgadmin/tools/restore/static/js/restore_dialog.js +++ b/web/pgadmin/tools/restore/static/js/restore_dialog.js @@ -43,8 +43,7 @@ export class RestoreDialog extends Dialog { const baseUrl = this.url_for_utility_exists(sid); // Check pg_restore utility exists or not. let that = this; - let service = axios.create({}); - service.get( + axios.get( baseUrl ).then(function(res) { if (!res.data.success) { diff --git a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js index 24d3666c4..6365a76ac 100644 --- a/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js +++ b/web/pgadmin/tools/restore/static/js/restore_dialog_wrapper.js @@ -140,8 +140,7 @@ export class RestoreDialogWrapper extends DialogWrapper { this.setExtraParameters(selectedTreeNode, treeInfo); - let service = axios.create({}); - service.post( + axios.post( baseUrl, this.view.model.toJSON() ).then(function (res) { diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index 41458445f..c01f2a9bd 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -35,6 +35,7 @@ define('tools.querytool', [ 'sources/sqleditor/calculate_query_run_time', 'sources/sqleditor/call_render_after_poll', 'sources/sqleditor/query_tool_preferences', + 'sources/csrf', 'sources/../bundle/slickgrid', 'pgadmin.file_manager', 'backgrid.sizeable.columns', @@ -49,7 +50,7 @@ define('tools.querytool', [ XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, GeometryViewer, historyColl, queryHist, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, - modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref) { + modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, csrfToken) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) return pgAdmin.SqlEditor; @@ -63,6 +64,8 @@ define('tools.querytool', [ HistoryCollection = historyColl.default, QueryHistory = queryHist.default; + csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); + var is_query_running = false; // Defining Backbone view for the sql grid. @@ -1892,6 +1895,7 @@ define('tools.querytool', [ var self = this; this.container = container; this.state = {}; + this.csrf_token = pgAdmin.csrf_token; // Disable animation first modifyAnimation.modifyAlertifyAnimation(); diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py index fd6ebe94b..26407a86e 100644 --- a/web/pgadmin/tools/user_management/__init__.py +++ b/web/pgadmin/tools/user_management/__init__.py @@ -22,6 +22,7 @@ import config from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_response as ajax_response, \ make_json_response, bad_request, internal_server_error +from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.model import db, Role, User, UserPreference, Server, \ ServerGroup, Process, Setting @@ -136,6 +137,7 @@ def script(): @blueprint.route("/current_user.js") +@pgCSRFProtect.exempt @login_required def current_user_info(): return Response( diff --git a/web/pgadmin/utils/csrf.py b/web/pgadmin/utils/csrf.py new file mode 100644 index 000000000..dfeb3a6a9 --- /dev/null +++ b/web/pgadmin/utils/csrf.py @@ -0,0 +1,43 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +######################################################################### + +from flask_wtf.csrf import CSRFProtect +from flask import request, current_app + + +class _PGCSRFProtect(CSRFProtect): + def __init__(self, *args, **kwargs): + super(_PGCSRFProtect, self).__init__(*args, **kwargs) + + def init_app(self, app): + res = super(_PGCSRFProtect, self).init_app(app) + self._pg_csrf_exempt(app) + + def _pg_csrf_exempt(self, app): + """Exempt some of the Views/blueprints from CSRF protection + """ + + exempt_views = [ + 'flask.helpers.send_static_file', + 'flask_security.views.login', + 'flask_security.views.logout', + 'pgadmin.tools.translations', + app.blueprints['redirects'], + 'pgadmin.browser.server_groups.servers.supported_servers-js', + 'pgadmin.tools.datagrid.initialize_query_tool', + 'pgadmin.tools.datagrid.panel', + 'pgadmin.tools.debugger.initialize_target', + 'pgadmin.tools.debugger.direct_new', + ] + + for exempt in exempt_views: + self.exempt(exempt) + + +pgCSRFProtect = _PGCSRFProtect() diff --git a/web/pgadmin/utils/session.py b/web/pgadmin/utils/session.py index 303d34d4c..7c0b6af28 100644 --- a/web/pgadmin/utils/session.py +++ b/web/pgadmin/utils/session.py @@ -153,7 +153,7 @@ class CachingSessionManager(SessionManager): with sess_lock: if sid in self._cache: session = self._cache[sid] - if session.hmac_digest != digest: + if session and session.hmac_digest != digest: session = None # reset order in Dict diff --git a/web/regression/python_test_utils/csrf_test_client.py b/web/regression/python_test_utils/csrf_test_client.py new file mode 100644 index 000000000..7c5cf8b19 --- /dev/null +++ b/web/regression/python_test_utils/csrf_test_client.py @@ -0,0 +1,124 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2019, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import re +import flask +from flask import current_app, request, session, testing + +from werkzeug.datastructures import Headers +from werkzeug.test import EnvironBuilder +from flask_wtf.csrf import generate_csrf +import config + + +class RequestShim(object): + """ + A fake request that proxies cookie-related methods to a Flask test client. + """ + def __init__(self, client): + self.client = client + + def set_cookie(self, key, value='', *args, **kwargs): + "Set the cookie on the Flask test client." + server_name = current_app.config["SERVER_NAME"] or "localhost" + return self.client.set_cookie( + server_name, key=key, value=value, *args, **kwargs + ) + + def delete_cookie(self, key, *args, **kwargs): + "Delete the cookie on the Flask test client." + server_name = current_app.config["SERVER_NAME"] or "localhost" + return self.client.delete_cookie( + server_name, key=key, *args, **kwargs + ) + + +class TestClient(testing.FlaskClient): + + def __init__(self, *args, **kwargs): + self.csrf_token = None + self.app = None + super(TestClient, self).__init__(*args, **kwargs) + + def setApp(self, _app): + self.app = _app + + def open(self, *args, **kwargs): + if len(args) > 0 and isinstance(args[0], (EnvironBuilder, dict)): + return super(TestClient, self).open(*args, **kwargs) + + data = kwargs.get('data', {}) + + if self.csrf_token is not None and not ( + 'email' in data and + 'password' in data and + 'csrf_token' in data + ): + api_key_headers = Headers({}) + api_key_headers[ + getattr(config, 'WTF_CSRF_HEADERS', ['X-CSRFToken'])[0] + ] = self.csrf_token + headers = kwargs.pop('headers', Headers()) + headers.extend(api_key_headers) + kwargs['headers'] = headers + + return super(TestClient, self).open(*args, **kwargs) + + def fetch_csrf(self, res): + m = re.search( + b'', res.data + ) + + return m.group(1).decode("utf-8") + + def generate_csrf_token(self, *args, **kwargs): + # First, we'll wrap our request shim around the test client, so + # that it will work correctly when Flask asks it to set a cookie. + request = RequestShim(self) + # Next, we need to look up any cookies that might already exist on + # this test client, such as the secure cookie that + # powers `flask.session`, + # and make a test request context that has those cookies in it. + environ_overrides = {} + self.cookie_jar.inject_wsgi(environ_overrides) + with self.app.test_request_context( + "/login", environ_overrides=environ_overrides, + ): + # Now, we call Flask-WTF's method of generating a CSRF token... + csrf_token = generate_csrf() + # ...which also sets a value in `flask.session`, so we need to + # ask Flask to save that value to the cookie jar in the test + # client. This is where we actually use that request shim we + # made! + self.app.save_session(flask.session, request) + + return csrf_token + + def login(self, email, password, _follow_redirects=False): + if config.SERVER_MODE is True: + res = self.get('/login', follow_redirects=True) + csrf_token = self.fetch_csrf(res) + else: + csrf_token = self.generate_csrf_token() + + res = self.post( + '/login', data=dict( + email=email, password=password, + csrf_token=csrf_token, + ), + follow_redirects=_follow_redirects + ) + self.csrf_token = csrf_token + + return res + + def logout(self): + res = self.get('/logout', follow_redirects=False) + self.csrf_token = None diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index 962b338b9..99cdb6dbe 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -51,8 +51,7 @@ def login_tester_account(tester): os.environ['PGADMIN_SETUP_PASSWORD']: email = os.environ['PGADMIN_SETUP_EMAIL'] password = os.environ['PGADMIN_SETUP_PASSWORD'] - tester.post('/login', data=dict(email=email, password=password), - follow_redirects=True) + tester.login(email, password) else: from regression.runtests import app_starter print("Unable to login test client, email and password not found.", @@ -61,18 +60,6 @@ def login_tester_account(tester): sys.exit(1) -def logout_tester_account(tester): - """ - This function logout the test account - - :param tester: test client - :type tester: flask test client object - :return: None - """ - - tester.get('/logout') - - def get_config_data(): """This function reads the server data from config_data""" server_data = [] @@ -802,7 +789,8 @@ def _cleanup(tester, app_starter): traceback.print_exc(file=sys.stderr) finally: # Logout the test client - logout_tester_account(tester) + tester.logout() + # Remove SQLite db file remove_db_file() if app_starter: diff --git a/web/regression/runtests.py b/web/regression/runtests.py index 6c10cac33..d5347d4b8 100644 --- a/web/regression/runtests.py +++ b/web/regression/runtests.py @@ -20,11 +20,13 @@ import sys import traceback import json import random +import flask from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + import unittest if sys.version_info[0] >= 3: @@ -57,6 +59,7 @@ if config.SERVER_MODE is True: config.SECURITY_CHANGEABLE = True config.SECURITY_POST_CHANGE_VIEW = 'browser.change_password' + from regression import test_setup from regression.feature_utils.app_starter import AppStarter @@ -95,6 +98,7 @@ from pgadmin.model import SCHEMA_VERSION # Delay the import test_utils as it needs updated config.SQLITE_PATH from regression.python_test_utils import test_utils +from regression.python_test_utils.csrf_test_client import TestClient config.SETTINGS_SCHEMA_VERSION = SCHEMA_VERSION @@ -105,16 +109,19 @@ config.CONSOLE_LOG_LEVEL = WARNING # Create the app app = create_app() -app.config['WTF_CSRF_ENABLED'] = False + app.PGADMIN_KEY = '' app.config.update({'SESSION_COOKIE_DOMAIN': None}) -test_client = app.test_client() driver = None app_starter = None handle_cleanup = None app.PGADMIN_RUNTIME = True if config.SERVER_MODE is True: app.PGADMIN_RUNTIME = False +app.config['WTF_CSRF_ENABLED'] = True +app.test_client_class = TestClient +test_client = app.test_client() +test_client.setApp(app) setattr(unittest.result.TestResult, "passed", [])