Fixed CSRF security vulnerability issue. per Alvin Lindstam. Fixes #4217

Initial patch by: Khushboo Vashi
Modified by: Ashesh Vashi and Murtuza Zabuawala
This commit is contained in:
Khushboo Vashi 2019-05-28 10:59:51 +05:30 committed by Akshay Joshi
parent 90a45557b9
commit 6f0eafb223
36 changed files with 387 additions and 124 deletions

View File

@ -15,6 +15,7 @@ Bug fixes
| `Bug #4164 <https://redmine.postgresql.org/issues/4164>`_ - Fix file browser path issue which occurs when client is on Windows and server is on Mac/Linux.
| `Bug #4194 <https://redmine.postgresql.org/issues/4194>`_ - Fix accessibility issue for menu navigation.
| `Bug #4208 <https://redmine.postgresql.org/issues/4208>`_ - Update the UI logo.
| `Bug #4217 <https://redmine.postgresql.org/issues/4217>`_ - Fixed CSRF security vulnerability issue.
| `Bug #4218 <https://redmine.postgresql.org/issues/4218>`_ - Properly assign dropdownParent in Select2 controls.
| `Bug #4219 <https://redmine.postgresql.org/issues/4219>`_ - Ensure popper.js is installed when needed.
| `Bug #4227 <https://redmine.postgresql.org/issues/4227>`_ - Fixed Tab key navigation for Maintenance dialog.

View File

@ -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).

View File

@ -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

View File

@ -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:

View File

@ -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() {});

View File

@ -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

View File

@ -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;

View File

@ -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:

View File

@ -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 = [

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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);
});
}

View File

@ -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,
})

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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');
},

View File

@ -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) {

View File

@ -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) {

View File

@ -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();

View File

@ -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(

43
web/pgadmin/utils/csrf.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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'<input id="csrf_token" name="csrf_token" type="hidden"'
b' value="([^"]*)">', 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

View File

@ -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:

View File

@ -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", [])